home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Aminet 21
/
Aminet 21 (1997)(GTI - Schatztruhe)[!][Oct 1997].iso
/
Aminet
/
dev
/
c
/
FW_ACMdev.lha
/
ACE-Devices
(
.txt
)
< prev
Encoding:
Amiga
Atari
Commodore
DOS
FM Towns/JPY
Macintosh
Macintosh JP
Macintosh to JP
NeXTSTEP
RISC OS/Acorn
Shift JIS
UTF-8
Wrap
Final Write Document
|
1997-07-18
|
928.2 KB
|
11,031 lines
Chapter 1 "DEVICES"
1.1 Introduction
1.2 Requests
1.2.1 IORequest
1.2.2 IOStdReq
1.2.3 IOExtReq
1.3 Prepare the Device
1.4 Standard Exec Commands
1.5 Send Requests
1.5.1 Synchronous Commands
1.5.2 Asynchronous Commands
1.5.2.1 Keep on Checking the Request Until It is Completed
1.5.2.2 Wait for the Request to be Completed
1.5.2.4 Wait for a Reply Message
1.5.3 Abort Requests
1.5.4 Quick I/O
1.6 Errors
1.7 Devices
1.7.1 Timer Device
1.7.2 Gameport Device
1.7.3 Audio Device
1.7.4 Narrator Device
1.7.5 Trackdisk Device
1.7.6 Serial Device
1.7.7 Parallel Device
1.7.8 Printer Device
1.7.9 Keyboard Device
1.7.10 Input Device
1.7.11 Console Device
1.7.12 Clipboard Device
1.7.13 SCSI and PCMCIA Device
Chapter 2 "TIMER DEVICE"
2.1 Introduction
2.2 Timer
2.2.1 Time Request
2.2.2 Open the Timer Device
2.2.3 Set Time Request
2.2.4 Clean Up
2.2.5 Example
2.3 System Time
2.3.1 Get System Time
2.3.2 Set System Time
2.4 Special Time Functions
2.4.1 Compare Times
2.4.2 Add Time
2.4.3 Subtract Time
2.4.5 Example
2.5 Functions
2.6 Examples
Chapter 3 "GAMEPORT DEVICE"
3.1 Introduction
3.2 Common Input Devices for the Gameport
3.2.1 Mouse
3.2.2 Joystick
3.2.3 Proportional Joystick
3.2.4 Light Pen
3.2.5 Drawing (Digitizing) Tablet
3.2.5 Trackball
3.3 The Gameport Device
3.3.1 Create a Message Port
3.3.2 Allocate an I/O Request Block (Structure)
3.3.3 Open the Gameport Device
3.3.4 Check if Some Other Task is Already Using The Port
3.3.5 Set Type of Controller
3.3.6 Set Trigger
3.3.7 Prepare to Read
3.4 How to Monitor the Gameport
3.4.1 The InputEvent Structure
3.4.1 Collect Joystick Events
3.4.2 Collect Mouse Events
3.5 Functions
3.6 Examples
Chapter 4 "AUDIO DEVICE"
4.1 Introduction
4.1.1 Sound
4.1.2 Different Waveforms
4.1.3 Digital and Analog Waveforms
4.1.4 Play Sampled Sounds or Create Your Own Tunes
4.2 Prepare the Audio Device
4.2.1 Priority
4.2.2 Allocating Channels
4.2.3 Create Waveforms
4.2.4 Notes and Frequencies
4.2.5 The Audio Request Block
4.2.6 Open the Audio Device
4.2.7 Reserve Channels
4.2.8 Lock Channels
4.3 Use the Audio Device
4.3.1 Play Sounds
4.3.2 Use Several Request Blocks
4.3.2 Play Double Buffered Sounds
4.3.3 Modify the Hardware Registers
4.4 Clean Up Afterwards
4.4.1 Unlock Channels
4.4.2 Remove All Messages and Close the Reply Port
4.4.3 Close the Device
4.4.4 Deallocate the Request Blocks
4.4.5 Deallocate Sound Buffers
4.5 Audio Device Commands
4.5.1 General Device Commands
4.5.1.1 CMD_Write
4.5.1.2 CMD_Read
4.5.1.3 CMD_Stop
4.5.1.4 CMD_Start
4.5.1.5 CMD_Flush
4.5.1.6 CMD_Reset
4.5.2 Special Audio Device Commands
4.5.1.1 ADCMD_
Allocate
4.5.1.2 ADCMD_F
ree
4.5.1.3 ADCMD_SETPREC
4.5.1.4 ADCMD_FINISH
4.5.1.5 ADCMD_PERVOL
4.5.1.6 ADCMD_LOCK
4.5.1.7 ADCMD_WAITCYCLE
4.6 Functions
4.7 Examples
Chapter 5 "NARRATOR DEVICE"
5.1 Introduction
5.2 Artificial Speech
5.2.1 Phonetic Symbols
5.2.2 Intonation
5.2.3 Punctuation and Special Symbols
5.2.4 Volume
5.3 Convert Text into Phonetic Symbols
5.3.1 Open the Translator Library
5.3.2 Translate Text
5.3.3 Close the Translator Library
5.4 Read Phonetic Symbols
5.4.1 Narrator Request Block
5.4.2 Open the Narrator Device
5.4.3 Read Phonetic Text
5.4.4 Using Several Request Blocks
5.4.5 Clean Up
5.4.5.1 Remove All Messages
5.4.5.2 Close Message Port
5.4.5.3 Close the Narrator Device
5.4.4 Deallocate the Request Blocks
5.5 The Narrator's Mouth
5.5.1 Mouth Request Block
5.5.2 Create a Mouth Request Block
5.5.3 Prepare the Mouth Request Block
5.5.4 Get the Size of the Mouth
5.6 Examples
Chapter 6 "TRACKDISK DEVICE"
6.1 Introduction
6.2 Amiga Disk Drivers
6.3 Trackdisk Device
6.3.1 RequestBlock
6.3.2 Open the Trackdisk Device
6.3.3 Clean Up
6.4 Commands
6.4.1 Read
6.4.2 Write
6.4.3 Motor On/Off
6.4.4 Update the Disk
6.4.5 Clear Buffer
6.4.6 Position the Head
6.4.7 Format
6.4.8 Remove
6.4.9 Get the Disk's Current Number
6.4.10 Check if There Is a Disk in the Drive or Not.
6.4.11 Check if the Disk Is Write-Protected or Not
6.4.12 Get Drive Type
6.4.13 Get the Number of Tracks
6.5 Errors
6.6 Examples
Chapter 7 "SERIAL DEVICE"
7.1 Introduction
7.2 The Serial Port
7.2.1 Byte to Bits and Vice-Versa
7.2.2 Pin Assignment
7.2.3 The Data Stream
7.3 The Serial Device
7.3.1 Prepare the Serial Device
7.3.2 Open the Serial Device
7.3.3 Set Serial Parameters
7.3.4 Read Data
7.3.5 Write Data
7.3.6 Errors
7.3.7 Clean Up
7.4 A Complete Example
7.5 Other Useful Commands
7.5.1 Break
7.5.2 Clear
7.5.3 Flush
7.5.4 Query
7.5.5 Reset
7.5.6 Start
7.5.7 Stop
7.6 Functions
7.7 Commands
7.8 Examples
Chapter 8 "PARALLEL DEVICE"
8.1 Introduction
8.2 Parallel Port
8.3 Parallel Device
8.3.1 The Parallel RequestBlock
8.3.2 Open the Parallel Device
8.3.3 Set Parallel Parameters
8.3.4 Write Data
8.3.5 Read Data
8.3.6 How to Handle Several Requests Simultaneously
8.3.7 Errors
8.3.8 Clean Up
8.4 A Complete Example
8.5 Other Useful Commands
8.5.1 Flush
8.5.2 Query
8.5.3 Reset
8.5.4 Start
8.5.5 Stop
8.6 Functions
8.7 Commands
8.8 Examples
Chapter 9 "PRINTER DEVICE"
9.1 Introduction
9.2 Printer Device
9.2.1 The Printer Device's RequestBlocks
9.2.3 Print Text
9.2.4 Send Special Commands to the Printer
9.2.5 Print Graphics
9.2.6 Errors
9.2.7 Clean Up
9.3 A Complete Example
9.4 Other Useful Commands
9.4.1 Flush
9.4.2 Reset
9.4.3 Start
9.4.4 Stop
9.5 Functions
9.6 Commands
9.7 Examples
TABLE OF CONTENTS
DEVICES
1.1 INTRODUCTION
The Amiga's operating system must be able to handle input events from the
keyboard or gameports, monitoring the disk drivers, sending data to the
parallel device and communicating with the serial port, playing stereo sound
with its audio chips, etc... If this was not enough, all these things have to
be monitored in a multitasking environment where several tasks (programs) may
run simultaneously.
As you understand this is a very complicated system that controls all these
things. Despite the complexity it must also be very fast so as little
processing time as possible is used.
The part of the Amiga which takes care of all these routines is usually
referred as "Exec". Exec handles tasks and task-switching, interrupts, messages
and controls all Amiga's "devices".
In this chapter we will look at how you can, with help of Exec, control the
devices. A device is a collection of special routines which handles things like
disk drivers, serial and parallel ports, input events, sound generating etc...
Although all these routines are very different they are all handled in almost
the same manner. This means that once you know how to control one of the
devices, you do not need to read much more before you can control all of them.
1.2 REQUESTS
When you want to do something with a device you fill a "request block" with
your requirements and send it to Exec. Exec will now take care of everything.
Once it has, successfully or not, completed your request it sends a message to
you which tells you that your request has been completed.
1.2.1 IOREQUEST
The "request block" is actually an IORequest structure defined in header file
"exec/io.h". It looks like this:
struct IORequest
{
struct Message io_Message;
struct Device *io_Device;
struct Unit *io_Unit;
UWORD io_Command;
UBYTE io_Flags;
BYTE io_Error;
};
io_Message:
The top part of the request block consists of a Message structure. It is used
by Exec when the request is sent around inside the Amiga. It is also used to
queue requests at a device's port. You do not need to bother very much about
this part since Exec takes care of most things. You must however initialize it
correctly before you may start to use the request.
io_Device:
Pointer to the device which this request has been prepared for. Se below for
more information about how to initialize a request block.
io_Unit:
Some devices, the trackdisk device for example, consists of several units
(df0:, df1, and so on), and this part is used to identify which unit should be
affected.
io_Command:
It is here you set your command. There exist both standard commands which are
handle by most devices, but there exist also a lot of special unique commands
for some devices. See below for more information about available commands.
io_Flags:
This field is used to set special flags.
io_Error:
When the request has been completed you can look at this field to see if your
request was successfully or not completed.
1.2.2 IOSTDREQ
The IORequest structure is the shortest request block you may use, and consists
of only the most important fields. Normally you need to use a request block
which is a bit larger. Since it is the most commonly used request block, it is
called the "standard request block" (IOStdReq) and is defined like this: (Also
declared in the header file "exec/io.h")
struct IOStdReq
{
struct Message io_Message;
struct Device *io_Device;
struct Unit *io_Unit;
UWORD io_Command;
UBYTE io_Flags;
BYTE io_Error;
ULONG io_Actual;
ULONG io_Length;
APTR io_Data;
ULONG io_Offset;
};
As you can see the top part is identical to the IORequest structure, but there
have been four extra fields added.
io_Actual:
When your request has been complete you can look here to find the actual number
of bytes transferred. If this value is not the same as the number of bytes you
wanted to transfer you know something went wrong.
io_Length:
The number of bytes you want to transfer. If you want to continue to
send/retrieve information until some special external event, set this field to
-1.
io_Data:
If you are going to send (write) data, this should be a pointer to the data
that should be sent. If you want to retrieve (read) data this should be a
pointer to a memory buffer were all data which is collected should be store.
io_Offset:
Special offset value used by some devices.
To make life simpler there exist a special function which will automatically
allocate and preinitialize the request block. The function is called
CreateStdIO(). However, before you may use this function you have to create a
message port to which the device can communicate with you. Simply use the
CreatePort() function which is described in the "System" manual, chapter
"Messages".
CreatePort() allocates and initializes a MsgPort structure. Normally the
message port should only be used by you and the device, so you do not need to
give it any name. The priority should usually be set to 0 (normal priority).
Synopsis
:
msg_port = CreatePort( name, pri );
msg_port:
(struct MsgPort *) Pointer to the new MsgPort structure, or NULL if something
went wrong.
name:
(char *) Pointer to a string containing the name of the message port, or NULL.
When working with devices you normally do not need to use any name.
msgp:
(struct MsgPort *) Pointer to the MsgPort structure that should be allocated.
pri:
(BYTE) This message port's priority. Usually set to 0 (normal priority).
Once you have successfully created the reply port you can allocate and
initialize the standard request block with help of the CreateStdIO() function.
Give it a pointer to the reply port as the only parameter.
Synopsis
:
std_req = CreateStdIO( msg_port );
std_req:
(struct IORequest *) Pointer to the new standard request block (struct IOStdReq
*), or NULL if the request block could not be created.
msg_port:
(struct MsgPort *) Pointer to the message port the device should use to
communicate with you.
When your program terminates you must delete all request blocks you have
created. Use the DeleteStdIO() function.
Synopsis
:
DeleteStdIO( std_req );
std_req:
(struct IOStdReq *) Pointer to the standard request block you want to delete.
All message ports must also be deleted, but remember to first deallocate the
standard request block to which the message port is connected to, before you
delete the message port itself.
Synopsis
:
DeletePort( msg_port );
msg_port:
(struct MsgPort *) Pointer to the MsgPort structure that should be deallocated.
Here is a short example:
/* Pointer to the standard request block: */
struct IOStdReq *std_req;
/* Pointer to the message (reply) port: */
struct MsgPort *msg_port;
/* 1. Create a message port so the device can */
/* communicate with us: (No name, normal priority) */
msg_port = CreatePort( NULL, 0 );
if( !msg_port )
clean_up( "ERROR! Could not create message port!" );
/* 2. Allocate and initialize a new standard request block. */
/* It should use our new message port as reply port: */
std_req = CreateStdIO( msg_port );
if( !std_req )
clean_up( "ERROR! Could not allocate the request block!" );
...
/* Deallocate standard request block: */
DeleteStdIO( std_req );
/* Close message port: */
DeletePort( msg_port );
1.2.3 IOEXTREQ
As if the extra IOStdReq structure would not be enough. There exist an even
larger request block, usually referred as the"extended request block". This
request structure always looks different depending on which device it is used
for. It always consists of a IORequest structure, or at least the same fields,
at the top, but the rest may vary. See the following chapters for more
information about this request block.
Since the size of this extended request block varies you can not use the
CreateStdIO() function to create and initialize it. Instead you have to use the
CreateExtIO() function which will allocate a request block of any specified
size. Except of allocating a larger request block it will do exactly the same
things as CreateStdIO(). (Remember that you first have to create a message
(reply) port as described above.)
Synopsis
:
ext_req = CreateExtIO( msg_port, size );
ext_req:
(struct IORequest *) Pointer to the new extended request block, or NULL if the
request block could not be created.
msg_port:
(struct MsgPort *) Pointer to the message port the device should use to
communicate with you.
size:
(long) The number of bytes that should be allocated for the extended request
block. Use the function sizeof() to find the exact number of bytes needed.
When your program terminates all request block have to be deleted. Extended
request blocks are deleted with the special DeleteExtIO() function. Once the
request block has been deleted you should also delete the message (reply) port.
Synopsis
:
DeleteExtIO( std_req, size );
std_req:
(struct IOStdReq *) Pointer to the extended request block you want to delete.
size:
(long) The size of the request block, in bytes.
Here is a short example: (In this example we will use an extended request block
designed for the "serial device".)
/* Pointer to the extended request block: */
struct IOExtSer *ext_req;
/* Pointer to the message (reply) port: */
struct MsgPort *msg_port;
/* 1. Create a message port so the device can */
/* communicate with us: (No name, normal priority) */
msg_port = CreatePort( NULL, 0 );
if( !msg_port )
clean_up( "ERROR! Could not create message port!" );
/* 2. Allocate and initialize a new extended request block. */
/* It should use our new message port as reply port, and */
/* have the same size as an IOExtSer structure: */
ext_req = CreateExtIO( msg_port, sizeof( struct IOExtSer ) );
if( !ext_req )
clean_up( "ERROR! Could not allocate the request block!" );
...
/* Deallocate extended request block: (Must be the same */
/* size as when allocated!) */
DeleteExtIO( ext_req, sizeof( struct IOExtSer ) );
/* Close message port: */
DeletePort( msg_port );
1.3 PREPARE THE DEVICE
Once you have created a request block you may "open" the device to which the
request block should be connected to. Note thatsome devices only need a
standard request block (or just a IORequest structure), but others may need
larger extended request blocks. See the following chapters for more
information.
To open a device simply use the OpenDevice() function.
Synopsis
:
error = OpenDevice( name, unit, req, flags );
error:
(long) If OpenDevice() managed to open the device it returns 0, else an error
number is returned.
name:
(char *) Name of the device you want to open. See the following chapters for
more information.
unit:
(long) Which unit you want to open. Some devices does not have several units
and this field is then ignored.
req:
(struct IORequest *) Pointer to a request block. Note that some devices may
need a larger extended request blocks.
flags:
(long) Special flags may sometimes be used here, otherwise this field is
ignored.
As usual, everything that you open must be closed before the program may
terminated. To close a device simply call the CloseDevice() function. After you
have closed the device you should delete the request block and then the message
port. (Unless you want to reopen the device later on.)
Synopsis
:
CloseDevice( req );
reg:
(struct IORequest *) Pointer to the device's request block.
1.4 STANDARD EXEC COMMANDS
There exist eight standard commands which most devices understand. If the
device is already occupied by some other request, your request is queued on a
FIFO basis (First In First Out). For some devices it may happen that the other
requests are aborted and your started if your request has higher priority.
The eight "standard" commands are:
CMD_RESET:
This command will reset the device. All currently queued requests are removed
(aborted) and the device is reinitialized to the default values.
CMD_READ:
This command tells the device to start reading (collecting) data. The
"io_Length" field of the request block states how many bytes should be read,
and the "io_Actual" field will contain the number of bytes actually
transferred.
CMD_WRITE:
This command tells the device to start writing (sending) data. The "io_Length"
field of the request block states how many bytes should be written, and the
"io_Actual" field will contain the number of bytes actually transferred.
CMD_UPDATE:
This command will update a device in that sense that any values still in the
internal buffers are written out. Most times this is automatically done, but
this command may be needed when you are working with the "trackdisk device" for
example.
CMD_CLEAR:
This command will clear any internal memory buffers.
CMD_STOP:
When this command is sent to a device all communication to and from that device
will be temporarily halted. Use the "CMD_START" command to restart the
communication.
CMD_START:
This command tells the device to restart the communication which has
temporarily been halted by a "CMD_STOP" command.
CMD_FLUSH:
If this command is sent to a device all currently queued requests will be
removed (aborted).
There exist several more commands, but those are device depending, and may
therefore only be used with a certain device. See the following chapters for
more information about these special device commands.
1.5 SEND REQUESTS
Once you have initialized the request block and "opened" the desired device,
which is described in more details in the following chapters, you only have to
send the request to Exec and the rest will automatically be done for you.
(Isn't that nice! At last you do not need to worry about everything.)
When you send requests to a device the commands can either be synchronous or
asynchronous. When you send synchronous commands your program is halted until
the request has been completed. With asynchronous commands your program will
continue to run while the device is executing your request.
1.5.1 SYNCHRONOUS COMMANDS
Synchronous commands are very easy to handle. You simply send your request with
the DoIO() function. Your program will automatically be put to sleep while the
device is executing your command, and once it has finished your program will
wake up. Since your program is put to sleep ("task sleep") no processing time
is waisted while you are waiting for your request to be completed.
Synopsis
:
error = DoIO( req );
error:
(long) DoIO() will return first when the request has been completed or
something has failed. If the request was successfully completed zero is
returned, else an error number is returned. What error number depends on which
device was used. See below for a complete list of standard error commands.
req:
(struct IORequest *) Pointer to the request you want to have executed.
1.5.2 ASYNCHRONOUS COMMANDS
Asynchronous requests are a bit more complicated to handle. Since your program
will continue to run while your request isbeing executed you do not know when
it has been completed. Asynchronous requests can be very useful when you do
not want to pause the program while waiting. (It would not be a very nice game
if it was halted each time the user did not move the joystick for example.)
You start asynchronous requests with help of the SendIO() function.
Synopsis
:
SendIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed.
After you have issued an asynchronous request there exist three different ways
to find out when it has been completed.
1.5.2.1 KEEP ON CHECKING THE REQUEST UNTIL IT HAS BEEN COMPLETED
You can use the CheckIO() function which will either return NULL if the request
has not been completed, or it will return a pointer to the request if it has
been completed.
Synopsis
:
ptr = CheckIO( req );
ptr:
(long) CheckIO() will either return NULL if the request have not been completed
or it will return a pointer to the request block.
req:
(struct IORequest *) Pointer to the request you want to check.
When a request has been completed a message is sent to a specified reply port.
Note that CheckIO() will not remove the message at the reply port. This should
be done with the Remove() function.
Synopsis
:
Remove( node );
node:
(struct Node *) Pointer to the node that should be removed.
1.5.2.2 WAIT FOR THE REQUEST TO BE COMPLETED
You can put your program to sleep and wait for the request to be completed, by
calling the WaitIO() function. WaitIO() willautomatically remove the message at
the reply port.
Synopsis
:
error = WaitIO( req );
error:
(long) WaitIO() will return first when the request, that has previously been
sent, has been completed or something has failed. If the request was
successfully completed zero is returned, else an error number is returned. What
error number depends on which device was used.
req:
(struct IORequest *) Pointer to the request you want to wait for to be
completed. Note that the request must have already been sent to the device by
either a SendIO() or BeginIO() function call. If the request has already been
completed WaitIO() will immediately return.
1.5.2.4 WAIT FOR A REPLY MESSAGE
Since a message is sent to the request block's reply port each time the request
has been completed you can simply wait for amessage to arrive. The simplest way
is to use the WaitPort() function which will put your program to sleep while
waiting for a message to arrive at the specified port. Note that it will not
remove the message.
Synopsis
:
msg = WaitPort( msg_port );
msg:
(struct Message *) Pointer to the first message which has arrived. Remember
that this function will not remove the message from the queue. Use the Remove()
function to do this.
msg_port:
(struct MsgPort *) Pointer to the message port which you want to monitor. Your
program will be put to sleep while waiting, and if no message arrive it will
never wake up, so be careful.
Instead of just waiting for one request to be completed you can wait fore
several ones at the same time. This is extremely useful if your program is
handling a lot of requests simultaneously. Now we need to use the multipurpose
Wait() function which is described in the "System" manual, chapter "Messages".
Synopsis
:
rsig = Wait( wsig )
rsig:
(ULONG) When one of the specified signal bits was received, the task is woken
up and Wait() returns the signal bit value that woke it up.
wsig:
(ULONG) The signals we are waiting for. Wait() will put the task to sleep, and
it will only wake up if one of the specified signal bit values is received.
1.5.4 QUICK I/O
Although the system with sending requests and messages is very fast it may
sometimes not be fast enough. If you want to handle a lot of data extremely
fast you can try to use the "Quick mode". When you are using this quick mode
Exec tries to skip all routines that are not essential. No messages are sent,
and there is no checking etc...
The quick mode should only be used when really needed, and it can not always be
used. When you want to try to use the quick mode set the IOF_QUICK flag in the
"io_Flags" field of the request block. To pass the request to Exec you should
not use the DoIO() or SendIO() functions, but instead the low level function
BeginIO().
Synopsis
:
BeginIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed. When you
want to use the quick mode you have to use this low level function rather than
SendIO() and DoIO().
If we were allowed to use the quick mode the request will be synchronous. (Your
program is put to sleep since all computer time is needed to run this fast
quick mode.)
If we were not allowed to use the quick mode the request will be asynchronous,
and is then handled exactly the same as if the request had been sent by a
SendIO() call.
After you have posted your quick request you must check if you were allowed to
use this fast mode or not. If not you have to wait for the request to be
completed and remove the reply message yourself. To check if you were allowed
quick mode or not simply look at the "io_Flags" field of the request block to
see if the flag IOF_QUICK is still there or not. If the flag is still set the
request was using the quick mode, and you do not need to do anything more. If
the flag is not there any more, you were not allowed quick mode, and thus you
have to wait for the asynchronous request to be completed and remove the
message yourself.
/* Initialize the request block as normal. */
/* The pointer to it is called "ioreq". */
/* ... */
/* Try to use the "quick" mode: */
ioreq->IOSer.io_Flags = IOF_QUICK;
/* Do the request: */
BeginIO( ioreq );
/* Check if the flag IOF_QUICK is still there or not: */
if( (ioreq->IOSer.io_Flags & IOF_QUICK) )
{
/* OK! The request was using quick mode, which means */
/* that our task was put to sleep while the request was */
/* executed, and the request has now been completed. */
/* Since we are using quick mode there are no message */
/* that should be removed. */
}
else
{
/* Too bad, we were not allowed to use the quick mode. */
/* The request should now be treated as if it was */
/* started with a SendIO() function call. This request */
/* is asynchronous - request returns immediately. */
/* Wait for the request to be completed. WaitIO() will */
/* also automatically remove the message at the reply */
/* port. */
WaitIO( ioreq );
}
1.5.3 ABORT REQUESTS
You can abort a previously started request by calling the AbortIO() function.
Of course, only asynchronous commands can be aborted, and if the request has
already been completed it can not be aborted any more.
Synopsis
:
AbortIO( req )
req:
(struct IORequest *) Pointer to the request you want to abort.
1.6 ERRORS
No one is perfect, and even the devices may sometimes not be able to do what
you tell them to do (usually because you have done something wrong). If
something goes fails an error number is returned. Each device has it own set of
unique error codes which are described in the following chapters. There exist,
however, four error codes that are sent by Exec. (They are all defined in the
header file "exec/errors.h")
IOERR_OPENFAIL:
The device (unit) could not be opened. If you receive this message there is
normally some other program currently using the device with exclusive mode.
IOERR_ABORTED:
When you abort a previously started request by calling the AbortIO() function,
the io_Error filed of that request is set to IOERR_ABORTED. If you find a
request block with this flag set, you know that it has been aborted.
IOERR_NOCMD:
You tried to use a command that is not supported by that device.
IOERR_BADLENGTH:
The length of the request was not valid.
1.7 DEVICES
There exist a lot of different devices each specialized to do something unique.
Some devices are so called "low level devices" and are handling a lot of fresh
untranslated raw data which will be modified and later used by other so called
"high level devices".
In total there exist 12 devices (14 with the new V2.05 operating system) which
all will be described in a chapter each. Although the devices are so different,
they all are handled in much the same manner - allocate a message port and a
request block, open the device and start to send your request, finally close
everything and quit.
Five devices are not covered in this edition, but will soon be added. All
registered members will receive more information about this as soon as the
chapters have been completed.
1.7.1 Timer Device
The Timer Device can be used if you want to receive a message after a specified
time limit. This is one of the so called "low level devices", and is used by
many other devices. Although the timer device does not sound very interesting,
it can really be useful.
1.7.2 Gameport Device
The Gameport Device is a smart little device which reads data from the two
gameport connectors on the Amiga. The device can handle both normal Joystick as
well as Mouse movements. Even light-pens and other input devices may be
connected and monitored by this device. These special input devices must
however have its own drivers which translates the signals to or the gameport
device understandable form.
1.7.3 Audio Device
The Audio Device is used to produce sound. It monitors the four hardware sound
channels, and can therefore play the sound in stereo. Everything from a simple
beep to complete songs may be played. The audio device can both produce sounds
itself or directly play sampled (digitized) sound effects.
Sound is an important part of our life and should therefore in my opinion also
be used a lot in computer programs. Sound can be used in most situations, from
emergency beeps to entertaining melodies.
1.7.4 Narrator Device
The Amiga is not only extremely good at producing high quality sound. with help
of the Narrator Device it can also speak almost as a human. Although it is not
a Frank Sinatra, it is possible to understand what it is saying, and this can
be used in many situations. Used with care, it is possible to produce very good
sound.
Sadly, somehow many persons do not like this unique feature of the Amiga, and
very few programs support the narrator device. I believe that artificial speech
can be used in many situations, and can really be handy for the user. Most
"professional" programs would really benefit from using the narrator device.
1.7.5 Trackdisk Device
With help of the Trackdisk Device you can directly access the disk drivers.
Data can not only be transferred to and from the disks, but you may even format
specific sectors of a disk and do a lot of other low level stuff.
1.7.6 Serial Device
The Serial Device is used to transfer data from and to the serial port. It can
be used at different speeds (bauds) and use seven or eight bit characters. The
device also support parity (error) checking.
1.7.7 Parallel Device
The Parallel device is used to transfer data from and to the parallel port. The
parallel device is very useful when you want very fast communications, and is
therefore often used by sound samplers and video digitizers. However, the most
commonly used external device is without question a printer, but you should
then use the printer device which has specially been designed for this purpose.
1.7.8 Printer Device
The printer device can use both the serial and parallel device depending on to
which port the printer is connected to. With the printer device you can not
only print normal text, specialprinter commands may be sent and you can even
print graphics with it.
The printer device uses the preferences settings to find out to which port the
printer is connected, what printer it is, and other special settings like
margins, paper length etc...
1.7.9 Keyboard Device
The Keyboard Device is a "low level device" which is used by the "Input
Device". The keyboard device collects untranslated (raw) key codes. This device
is very fast and uses very little computer time.
This device will be covered in the next version of the manual. All registered
members will receive more information as soon as the next version has been
completed.
1.7.10 Input Device
The Input Device is a so called "high level device" which receives information
from the timer, keyboard and gameport devices, and puts all this information
into one single input stream. Very handy and useful device.
This device will be covered in the next version of the manual. All registered
members will receive more information as soon as the next version has been
completed.
1.7.11 Console Device
The Console Device enables you to handle input and output as an intelligent
terminal for ASCII characters. Works very close with Intuition, and is also a
very handy and useful device.
This device will be covered in the next version of the manual. All registered
members will receive more information as soon as the next version has been
completed.
1.7.12 Clipboard Device
With the Clipboard Device data can be cut, pasted and copiedbetween different
programs. Several clips may be used simultaneously, and the device supports the
IFF standard.
This device will be covered in the next version of the manual. All registered
members will receive more information as soon as the next version has been
completed.
1.7.13 SCSI AND PCMCIA DEVICE
There exist two new devices. The SCSI device is only available with the new
V2.0 operating system, and the PCMCIA device exist for the moment only on Amiga
600.
These device will be covered in the next version of the manual. All registered
members will receive more information as soon as the next version has been
completed.
TIMER DEVICE
2.1 INTRODUCTION
The timer device may not sound very exciting but it can be extremely useful.
Many so called "high level" devices are using the timer device, and very often
you need to use it yourself. It is not very accurate since other tasks running
at the sametime may delay the time reports, but is usually good enough, and in
the long run it is very accurate.
The timer device is a very simple device. It's main task is to send messages to
all its users after specified time periods. The timer device can also compare
different time periods, and add as well as subtract one time period with
another. No thrills, but useful stuff.
2.2 TIMER
The timer device's main task is to send time reports to all its users. It is
controlled like all other devices by sending request blocks, and is therefore
easy to handle. See previous chapter "Devices" for more information about
devices and request blocks.
2.2.1 TIME REQUEST
The timer device's request block is an "extended" request block and is defined
in header file "devices/timer.h":
struct timerequest
{
struct IORequest tr_node;
struct timeval tr_time;
};
tr_node:
As with all request blocks, the top part consists of an IORequest structure
which is used by Exec to handle the requests. While using the timer device you
do not need to use this structure (well, at least not very often). Simply let
the system take care of it.
tr_time:
The second part of the request block consists of a timeval structure: (Also
defined in the header file "devices/timer.h".)
struct timeval
{
ULONG tv_secs;
ULONG tv_micro;
};
tv_secs:
The number of seconds you want to wait before the time request should be
completed.
tv_micro:
The number of microseconds. Note that the timer device is not very accurate.
Your requests can be delayed by other programs running at the same time, but in
the long run the timer device is accurate enough.
As with all devices you have to connect a message port to the request block
with which the timer device can talk to you. Create a message (reply) port by
calling the CreatePort() function. Normally you should not use any name (the
message port should not be made public) and priority set to 0 (normal
priority).
Synopsis
:
msg_port = CreatePort( name, pri );
msg_port:
(struct MsgPort *) Pointer to the new MsgPort structure, or NULL if something
went wrong.
name:
(char *) Pointer to a string containing the name of the message port, or NULL.
When working with devices you normally do not need to use any name.
pri:
(BYTE) This message port's priority. Usually set to 0 (normal priority).
Once you have successfully opened a message port you may allocate and
initialize the request block. Since the request block is "extended" (not the
same size as the "standard" - IOStdReq structures) you need to use the
CreateExtIO() function to allocate and initialize the request block.(Do not use
the CreateStdIO() function!)
Synopsis
:
ext_req = CreateExtIO( msg_port, size );
ext_req:
(struct IORequest *) Pointer to the new extended request block, or NULL if the
request block could not be created.
msg_port:
(struct MsgPort *) Pointer to the message port the CreatePort() function
returned. The timer device will use it to send messages to you.
size:
(long) The number of bytes that should be allocated for the extended request
block. Use the function sizeof() to find the exact number of bytes needed.
Example: sizeof( struct timerequest ).
Here is an example:
/* The reply port: */
struct MsgPort *replymp;
/* The timer request block: */
struct timerequest *timer_req;
...
/* Get a reply port: (No name, normal priority) */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* Allocate and preinitialize a request block: */
timer_req = (struct timerequest *)
CreateExtIO( replymp, sizeof(struct timerequest) );
if( !timer_req )
clean_up( "Could not create the request block!" );
2.2.2 OPEN THE TIMER DEVICE
Once you have successfully created a request block you can "open" the timer
device. You should, however, first decide how accurate you want the timer
device to be. The timer device can work in two different modes:
1. The timer device can use the video beam to control its own timer. This will
make the timer very stable over long time periods, but is not very accurate for
short intervals (less than one second).
This mode is easy for the timer device to handle and thus little computer time
is used. This mode is called "vertical blank timer" and the flag is
"UNIT_VBLANK".
2. If you want to use very short time periods (less than one second) it can
sometimes be necessary to use the special CIA chips for the timer. The CIA
chips has a much higher "resolution" than the video beam, thus it will result
in very accurate time periods for short intervals. This mode is more
complicated than the vertical blank timer, and more computer time is therefore
used. This mode is called "micro hertz timer" and the flag is "UNIT_MICROHZ"
The general rule is that time periods longer than one second should use the
vertical blank timer. Else, if really high "resolution" is needed, the micro
hertz timer should be used.
Once you have decided which mode should be used you can open the timer device.
No frills here, simply use the OpenDevice() function.
Synopsis
:
error = OpenDevice( name, unit, req, flags );
error:
(long) If OpenDevice() managed to open the timer device it returns 0, else an
error number is returned.
name:
(char *) Name of the device you want to open. The name for the timer device has
been defined as "TIMERNAME" (header file "devices/timer.h").
unit:
(long) Which timer mode you want the device to use. Set this field to either
"UNIT_VBLANK" or "MICROHZ".
req:
(struct IORequest *) Pointer to a previously initialized request block.
flags:
(long) Special flags which are ignored by the timer device.
Here is an example:
/* Open the Timer Device: ("vertical blank timer") */
error = OpenDevice( TIMERNAME, UNIT_VBLANK, timer_req ,0 );
if( error )
clean_up( "Could not open the Timer Device!" );
2.2.3 SET TIME REQUEST
When the timer device has been opened you can start to send your time requests.
You specify the number of seconds and microseconds that should pass before a
message will be sent by initializing the timeval structure. Note that the time
you set is the minimum time. When the exact time has elapsed it may happen that
some other program is for the moment occupying the processor, and thus the time
message will be delayed for some microseconds.
You must also tell the device that you are sending a time request. You do it by
setting the flag "TR_ADDREQUEST" in the "io_Command" field.
The request can now be sent to the device. Either use the DoIO() function if
you want to wait for the request to be completed, or use the SendIO() function
if you want your program to continue to run while the request is executed. See
the previous chapter for a complete explanation on when and how the DoIO() and
SendIO() functions should be used.
Here is an example:
/* Set time: (5.25 seconds)*/
timer_req->tr_time.tv_secs = 5;
timer_req->tr_time.tv_micro = 250000;
/* We want to add a time request: */
timer_req->tr_node.io_Command = TR_ADDREQUEST;
/* Do our request and return when done: */
/* (Our task is put to sleep for 5.25 sec.) */
DoIO( timer_req );
If you want to send multiple requests you have to use several request blocks
which each has been properly initialized. You must then also use the function
SendIO() and not DoIO() since your program will else be put to sleep directly
after the first request has been sent. See example 2.
Each time you send a request to the timer device all values in the timeval
structure are destroyed. You must therefore always reinitialize these fields
before you may repost the request to device.
2.2.4 CLEAN UP
As always you must remember to clean up after you. All allocated request blocks
must be removed, all opened message ports closed as well as all devices.
Fist you should close the timer device. Note that all requests you have posted
to that device must either have been completed or aborted before you may close
the device! All devices are closed by the special CloseDevice() function.
Synopsis
:
CloseDevice( req );
reg:
(struct IORequest *) Pointer to the timer device's own request block. (The same
request block which was used when you opened the timer device.)
The request blocks are deleted by calling the DeleteExtIO() function. Note that
the request must have been completed or aborted before you may delete it! Note
also that you can not use the DeleteStdIO() function since the timer device is
using extended request blocks.
Synopsis
:
DeleteExtIO( std_req, size );
std_req:
(struct IOStdReq *) Pointer to the request block you want to delete.
size:
(long) The size of the request block, in bytes. Use the function sizeof() to
get the correct number of bytes. Example: sizeof( struct timerequest ).
Finally you should close all message ports by calling the DeletePort()
function. All messages must have been removed before you may close the message
port.
Synopsis
:
DeletePort( msg_port );
msg_port:
(struct MsgPort *) Pointer to the MsgPort structure that should be deleted.
Here is a short example:
/* Close the Timer Device: */
CloseDevice( timer_req );
/* Delete the request block: */
DeleteExtIO( timer_req, sizeof( struct timerequest) );
/* Remove the message port: */
DeletePort( replymp );
2.2.5 EXAMPLE
Here is a complete example on how to use the timer device. It will open a
message port, create a request block, open the timerdevice, and finally put
itself to sleep for 10 seconds. When the time has elapsed everything is
returned and the program terminates. (Hmmm, very similar to example 1.)
#include <exec/types.h> /* STRPTR */
#include <exec/ports.h> /* struct Message */
#include <exec/memory.h> /* MEMF_PUBLIC */
#include <devices/timer.h> /* TIMERNAME */
/* The reply port: */
struct MsgPort *replymp;
/* The timer request block: */
struct timerequest *timer_req;
/* When the Timer Device is open this variable is TRUE: */
BOOL not_opened = TRUE;
/* Declare our functions: */
void clean_up();
void main();
void main()
{
/* 1. Get a reply port: */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* 2. Get an extended request block: */
timer_req = (struct timerequest *)
CreateExtIO( replymp, sizeof( struct timerequest) );
if( !timer_req )
clean_up( "Could not create the IO!" );
/* 3. Open the Timer Device: */
not_opened = OpenDevice( TIMERNAME, UNIT_VBLANK, timer_req ,0 );
if( not_opened )
clean_up( "Could not open the Timer Device!" );
/* 4. Set our request: */
timer_req->tr_node.io_Command = TR_ADDREQUEST; /* Add a request. */
timer_req->tr_time.tv_secs = 10; /* 10 seconds. */
timer_req->tr_time.tv_micro = 0; /* 0 micro seconds. */
/* 5. Do our request: (Wait 10 seconds) */
printf( "Good night...\n" );
DoIO( timer_req );
printf( "Good morning!\n" );
/* Clean up and quit: */
clean_up( "The End!" );
}
void clean_up( text )
STRPTR text;
{
/* Close the Timer Device: */
if( !not_opened )
CloseDevice( timer_req );
/* Delete the extended request block: */
if( timer_req )
DeleteExtIO( timer_req, sizeof( struct timerequest) );
/* Remove the message port: */
if( replymp )
DeletePort( replymp);
/* Print the last message: */
printf( "%s\n", text );
/* Quit: */
exit( 0 );
}
2.3 SYSTEM TIME
The timer device can also be used to get the current system time. It should be
noted that this system time has nothing to do with the Amiga's internal clock.
The system time is simply a value which is initialized to the date when the
boot disk was last modified. The value is relative to 1 January 1978, time
00:00:00.
Although it may not be a "correct" time value it can be useful. For example, if
you want to adjust several time periods with each other it can be handy to have
an external timer like the system time.
With help of the timer device you can both get the current system time as well
as set it to a new value.
2.3.1 GET SYSTEM TIME
To get the current system time you set the flag "TR_GETSYSTIME" in the
io_Command field of the request block and send therequest block to the timer
device. When the timer device has completed the request it is returned and you
may now examine the timeval structure where the current system time has been
stored.
Each time you get the system time you will receive a value which is different
from previous calls (it is an unique value).
Here is a short example:
/* Get the current system time: */
timer_req->tr_node.io_Command = TR_GETSYSTIME;
/* Do the request: */
DoIO( timer_req );
/* Print the current system time: */
printf( "The current system time is:\n" );
printf( "Seconds: %d\n", timer_req->tr_time.tv_secs );
printf( "Microseconds: %d\n", timer_req->tr_time.tv_micro );
2.3.2 SET SYSTEM TIME
To set the system time to a new value simply set the "TR_SETSYSTIME" flag in
the io_Command field of the request block, set the new time in the timeval
structure, and finally send the request block to the timer device.
You may only set values that are greater than the current time. The timer
device or other programs may be confused if the time suddenly went backwards.
Here is a short example:
/* Add two hours and 0.4 seconds: */
tr->tr_time.tv_secs += 60 * 60 * 2;
tr->tr_time.tv_micro += 400000;
/* Set the new system time: */
tr->tr_node.io_Command = TR_SETSYSTIME;
/* Do the request: */
DoIO( tr );
2.4 SPECIAL TIME FUNCTIONS
The timer device can also be used to compare different time reports and to add
or subtract time values. To do these things the timer device contain three
functions - CmpTime(), AddTime() and Subime(). Since these features are
accessed as functions rather than using request blocks you have to prepare the
timer device a little before you may use these functions.
The timer device is using a global pointer which is always called "TimerBase".
This global pointer is used to find the starting address of the three special
timer functions. Before you may use these functions you must therefore declare
and initialize the TimerBase pointer. To initialize it simply use any request
block which is connected to the timer device. In the "io_Device" field of the
request block you will find the current address value of the timer device.
Here is a short example:
/* Declare the global timer pointer: */
struct Device *TimerBase;
/* ... create request blocks, open timer device etc ... */
/* Get the global pointer to the timer device: */
TimerBase = timer_req->tr_node.io_Device;
Once you have found the TimerBase pointer you may start to use the timer
functions. Their primary usage is to coordinate different time requests with
each other.
2.4.1 COMPARE TIMES
The CmpTime() function is used to compare two timeval structures and returns 0
if they were identical, -1 if the first timeval structure was greater than the
second, and 1 if the opposite (the second timeval structure was greater than
the first).
Synopsis
:
dif = CmpTime( time1, time2 );
dif:
(long) If the two time values were identical 0 is returned. If the first time
value was greater than the second time value -1 is returned, and if the
opposite (the second time value is greater than the first time value) 1 is
returned.
time1:
(struct timeval *) Pointer to the first timeval structure.
time1:
(struct timeval *) Pointer to the second timeval structure.
2.4.2 ADD TIME
The AddTime() function is used to add the time in one timeval structure with
the time in another timeval structure.
Synopsis
:
AddTime( tiem1, time2 );
time1:
(struct timeval *) Pointer to the first timeval structure. The value of the
second timeval structure will be added with this timeval structure, and the
result stored here.
time2:
(struct timeval *) Pointer to the second timeval structure.
2.4.3 SUBTRACT TIME
The SubTime() function is used to subtract the time in one timeval structure
with the time in another timeval structure.
Synopsos:
SubTime( tiem1, time2 );
time1:
(struct timeval *) Pointer to the first timeval structure. The value of the
second timeval structure will be subtracted with this timeval structure, and
the result stored here.
time2:
(struct timeval *) Pointer to the second timeval structure.
2.4.5 EXAMPLE
This short example demonstrates how to use the timer device's own functions.
("tr1", "tr2" and "tr3" are three alreadyinitialized requst blocks.)
/* Set the times: */
/* Request 1: */
tr1->tr_time.tv_secs = 10;
tr1->tr_time.tv_micro = 0;
/* Request 2: */
tr2->tr_time.tv_secs = 7;
tr2->tr_time.tv_micro = 0;
/* Request 3: */
tr3->tr_time.tv_secs = 5;
tr3->tr_time.tv_micro = 0;
/* Get a pointer to the timer device: */
TimerBase = tr1->tr_node.io_Device;
/* Compare the first two time val structures: */
printf( "Compare time1 - time2: %d\n",
CmpTime( &(tr1->tr_time), &(tr2->tr_time) ) );
/* Add the time in the third timeval structure to */
/* the second timeval structure. The second timeval */
/* structure should now contain the time 12 sec. */
AddTime( &(tr2->tr_time), &(tr3->tr_time) );
/* Subtract the time in the third timeval structure with */
/* the second timeval structure. The second timeval */
/* structure should now again contain the time 7 seconds. */
SubTime( &(tr2->tr_time), &(tr3->tr_time) );
Note that we have to use pointers to the timeval structures
as arguments [&(tr1->tr_time]. The parentheses around the
expression are actually unnecessary since the "->" operator
already has higher priority than the "&" operator. However,
this makes it easier to read.
2.5 FUNCTIONS
The functions DoIO(), SendIO(), WaitIO(), AbortIO() etc... have already been
defined in the previous chapter, and I will therefore not repeat all this
information here.
There exist three functions which are supported by the timer device. Before you
may use them you have to initialize a global pointer named "TimerBase". (See
above for more information.)
CmpTime()
The CmpTime() function is used to compare two timeval structures and returns 0
if they were identical, -1 if the first timeval structure was greater than the
second, and 1 if the opposite (the second timeval structure was greater than
the first).
Synopsis:
dif = CmpTime( time1, time2 );
dif:
(long) If the two time values were identical 0 is returned. If the first time
value was greater than the second time value -1 is returned, and if the
opposite (the second time value is greater than the first time value) 1 is
returned.
time1:
(struct timeval *) Pointer to the first timeval structure.
time2:
(struct timeval *) Pointer to the second timeval structure.
AddTime()
The AddTime() function is used to add the time in one timeval structure with
the time in another timeval structure.
Synopsis:
AddTime( tiem1, time2 );
time1:
(struct timeval *) Pointer to the first timeval structure. The value of the
second timeval structure will be added with this timeval structure, and the
result stored here.
time2:
(struct timeval *) Pointer to the second timeval structure.
SubTime()
The SubTime() function is used to subtract the time in one timeval structure
with the time in another timeval structure.
Synopsos:
SubTime( tiem1, time2 );
time1:
(struct timeval *) Pointer to the first timeval structure. The value of the
second timeval structure will be subtracted with this timeval structure, and
the result stored here.
time2:
(struct timeval *) Pointer to the second timeval structure.
2.6 EXAMPLES
Example 1
This example demonstrates how you can use the Timer Device. The program will be
put to sleep for 10 seconds.
Example 2
This example demonstrates how you can send several requests to the Timer
Device.
Example 3
This example demonstrates how you can use the Timer Device to get the current
system time. We will then add two hours and set the new system time.
Example 4
This example demonstrates how you can compare, add and subtract time values
with help of the timer device's own functions.
GAMEPORT DEVICE
3.1 INTRODUCTION
All Amiga models have two contacts to which you can connect extra input devices
like a mouse, joystick, trackball, proportional joystick etc. If Intuition is
used, the left gameport is reserved for a mouse that controls the pointer.
However, the right port can be used freely.
To monitor these two gameports you can uses the Gameport Device. It is a clean
and polite way of controlling the gameports. You can of course go directly on
the hardware and check what is happening. This is both easier and faster, but
should only be used in games since it is not so polite to the system.
If you go directly on the hardware your program will be machine dependent, and
may not run on future models of the Amiga.
I doubt that Commodore ever will try to change the hardware position of the
gameport, but you never know. Programs that use the Gameport device will on the
other hand always work, even if an Amiga 4000 is released. So if you can use
the Gameport Device instead of hitting the hardware directly, stick to the
device.
3.2 COMMON INPUT DEVICES FOR THE GAMEPORT
To the gameports you can connect several types of input devices, like:
- Mouse
- Joystick
- Proportional (analogue) joystick
- Light pen
- Drawing (Digitizing) tablet
- Trackball
The most commonly used input devices is undoubtedly the mouse and joystick.
However, inputdevices like analogue joysticks, light pens and digitizing
tablets are getting more and more popular.
It is very common that a joystick is already connected to the right port. Most
games expect that, but it does not mean that you can not connect a second mouse
or proportional joystick there. It is a good rule to always tell the user what
type of input device he/she should use, before your program starts.
3.2.1 MOUSE
The mouse is the most commonly used device since all Amigas are old together
with one. It is perfect for positioning cursors (like Intuition's pointer), or
to move a gun-sight over some enemies.
A mouse usually consists of a rubber ball covered by a box with one or more
buttons on top. When the user is sliding the mouse, the rubber ball is rotated
and delta x and y movements are reported.
The normal Amiga mouse comes with two buttons, but there exist those with only
one or three buttons.
3.2.2 JOYSTICK
A normal joystick (also called absolute joystick) can only report 8 stick
movements like: up, right, down, left and possible combinations like up and to
the right. A joystick can have one or two buttons. Sadly most joystick comes
with only one button.
3.2.3 PROPORTIONAL JOYSTICK
A proportional (or analog) joystick is like a normal absolute joystick with one
difference, it will report the exact delta position on the stick. The further
in one direction, the higher number is broadcasted.
Proportional joysticks are perfect for flight simulators and similar programs.
They are becoming more available nowadays, so it is a good idea to start
supporting this type of device.
The Gameport Device can sadly not handle proportional joysticks for the moment
which is a pity. However, I have included a small program in the "Input" drawer
called "Analogue" which reads proportional (analogue) joystick movements
directly from the hardware registers.
3.2.4 LIGHT PEN
A light pen is a small device with which you can point anywhere on the screen
in order to position a cursor or select an option. There have during the last
years been a big discussion about which device, a mouse or a light pen, is
easiest to use. I think it depends on what you are going to do. A pen is
undoubtedly more natural for a beginner, but your arm gets very tired after
some minutes, so it should not be used to much.
Luckily you do not need to bother about the light pen since it is also sadly
not supported by the Gameport Device. However, newly made light pens usually
comes with some software that replaces Intuition's mouse with the light pen. If
you write your program as normal and is monitoring a mouse, the user can still
connected and use a light pen. Your program whould not notice any difference.
3.2.5 DRAWING (DIGITIZING) TABLET
Drawing (Digitizing) tablet is used in the same manner as a light pen, but with
the advantage that your arm does not get so tired, and with the disadvantage
that all your free space on your desk is suddenly occupied by a large sensitive
tablet. (It is not recommended to spill hot coffee on it for example.)
Drawing tablets are not either supported by the Gameport Device. However, as
same as with the light pen, most digitizing tablets comes with some software
that replaces the normal mouse driver with its own driver. So if your program
supports a mouse, it can also handle most digitizing tablets.
3.2.5 TRACKBALL
A trackball is working exactly as a mouse, but with the advantage that you do
not have to move it around. Since it acts exactly as a mouse you do not have to
bother about any special routines for handling trackballs. Simply support mouse
events and the user can connect a trackball instead.
3.3 THE GAMEPORT DEVICE
The Gameport Device helps you to monitor the Amiga's two gameports. Port 1
(unit 0) is usually already occupied by Intuition, but port 2 (unit 1) can
normally be used. The Gameport Device supports for the moment only two types of
input devices, and they are:
1. Mouse (Trackball)
2. Joystick
To prepare the Gameport Device to handle input events you have to:
1. Create a message port with which the Gameport Device can communicate with
you. [CreatePort()]
2. Allocate an input/output request block (structure). [CreateStdIO()]
3. Open the Gameport Device [OpenDevice()]
4. Check if some other task is already using the gameport. (A gameport can only
be monitored by one task.)
5. Tell the Gameport Device what type of input device you want to monitor. (For
the moment you can only monitor mouse or joystick events.)
6. Tell the Gameport Device what events should be reported, and when.
7. Tell the Gameport Device that we want to start collecting gameport events.
3.3.1 CREATE A MESSAGE PORT
As normal when you are using a device, you first have to create message port
with which the device can communicate with you. The simplest way is to create a
message port by calling the CreatePort() function, which will return a pointer
to a MsgPort structure (defined in the headerfile "exec/ports.h").
CreatePort() allocates and initializes a MsgPort structure. If you give it a
string as first parameter it will also make the port public. If CreatePort() of
some reason could not create a message port it returns NULL, otherwise if
everything is OK it returns a pointer to the new MsgPort structure.
Synopsis
:
msgp = CreatePort( name, pri );
msgp:
(struct MsgPort *) Pointer to the new MsgPort structure, or NULL if something
went wrong.
name:
(char *) Pointer to a string containing the name of the message port, or NULL.
If it is a string the port will be made public (so other tasks can find it)
else only our task can use it.
msgp:
(struct MsgPort *) Pointer to the MsgPort structure that should be allocated.
pri:
(BYTE) This message port's priority. Usually set to 0 - normal priority.
When your program terminates you must close the message port by calling the
DeletePort() function!
DeletePort() deletes a message port. Every message port that has been allocated
by CreatePort() must be deleted before the program terminates.
Synopsis
:
DeletePort( msgp );
msgp:
(struct MsgPort *) Pointer to the MsgPort structure, that should be
deallocated.
Here is an example:
/* Declare a pointer to our message port: */
struct MsgPort *game_msg_port;
/* Create the message port: */
/* (No name, priority 0.) */
game_msg_port = CreatePort( 0, 0 );
/* Check if we have received a message port or not: */
if( !game_msg_port )
/* ERROR! Could not create message port! */
...
/* Close message port: */
DeletePort( game_msg_port );
3.3.2 ALLOCATE AN INPUT/OUTPUT REQUEST BLOCK (STRUCTURE)
Secondly you need a I/O request structure (IOStdReq) in which you can store
information like where the data is that shouldbe processed and how much data
you send. When the Gameport Device has replied you can find error message, if
any, in the structure etc. More about this later.
The IOStdReq structure should also be linked together with our message port. We
will then receive a message each time the Gameport Device returns our I/O
request.
The best way to allocate an IOStdReq structure is to call the CreateStdIO()
function which will both allocate and pre-initialize the structure. (The
IOStdReq structure is defined in the headerfile "exec/io.h".) We give
CreateStdIO() a pointer to our message port, so it will automatically be linked
together.
CreateStdIO() allocates and initializes a new IOStdReq structure. As only
parameter you give it a pointer to a message port that will be used by the
system to communicate with you. If the function succeeds, it returns a pointer
to the new IoStdReq structure, else NULL which means something went wrong.
Synopsis
:
io = CreateStdIO( msgp );
io:
(struct IOStdReq *) Pointer to the new IOStdReq structure, or NULL if something
went wrong.
msgp:
(struct MsgPort *) Pointer to a MsgPort structure with which the system can
communicate with us.
Before your program terminates you must deallocate the request block by calling
the DeleteStdIO() function.
DeleteStdIO() deallocates a IOStdReq structure that has been created by
CreateStdIO(). Note that all allocated structures must be deleted before the
program may terminate.
Synopsis
:
DeleteStdIO( io );
io:
(struct IOStdReq *) Pointer to the a IOStdReq structure that should be
deallocated.
Here is an example:
/* Declare a pointer to our I/O request block: */
struct IOStdReq *game_io_msg;
/* Allocate and initialize a new I/O request block: */
/* (It should use our new message port as reply port.) */
game_io_msg = CreateStdIO( game_msg_port );
/* Check if we have allocated the req. block successfully: */
if( !game_io_msg )
/* ERROR! Could not allocate new I/O request block! */
...
/* Deallocate I/O request block: */
DeleteStdIO( game_io_msg );
3.3.3 OPEN THE GAMEPORT DEVICE
Now you can open the Gameport Device with help of the OpenDevice() function.
When you open this device you must also tell the system which port you want to
monitor (unit 0 - the "mouse port" or unit 1 - the "joystick" port). The
function will return 0 if everything is OK, or an error number if it could not
open the Gameport Device.
OpenDevice() will try to open the specified device. It will return 0 if
everything went OK, else an error number is returned.
Synopsis
:
error = OpenDevice( name, unit, io, flags );
error:
(long) OpenDevice() will return 0 if everything went OK, or an error number if
something went wrong.
namne:
(char *) Pointer to a string containing the name of the device you want to
open. The name of the Gameport Device is "gameport.device".
unit:
(long) Which unit should be used. The Gameport Device can handle two ports:
unit 0 - the left "mouse port", port 1.
unit 1 - the right "joystick port", port 2.
io:
(struct IORequest *) Pointer to an already initialized IORequest or IOStdReq
structure.
flags:
(long) Additional information, set to 0.
Before your program terminates you have to close the Gameport Device by calling
the CloseDevice() function. CloseDevice() tries to close a device that has
previously been opened by an OpenDevice() call.
Synopsis
:
CloseDevice( io );
io:
(struct IORequest *) Pointer to an IORequest or IOStdReq structure that has
been used as parameter in the OpenDevice() function call.
Here is an example:
/* Declare a boolean variable which will be TRUE if */
/* something when wrong, or FALSE if everything is OK: */
BOOL deviceerror;
/* Open the Game Port Device, use the right port (unit 1): */
deviceerror =
OpenDevice( "gameport.device", 1, game_io_msg, 0 );
/* Check if we have opened the device or not: */
if( deviceerror )
clean_up( "ERROR! Could not open the Game Port Device!" );
...
/* Close the Gameport Device: */
CloseDevice( game_io_msg );
3.3.4 CHECK IF SOME OTHER TASK IS ALREADY USING THE PORT
It is now important to check if some other task is already monitoring the port,
and if so we can not try to use it ourself.
Set the io_Command field of the IOStdReq structure to GPD_ASKCTYPE. This tells
the system that you want to know what type of controller, if any, is for the
moment using the port. We must now also give the structure (io_Data) a pointer
to at least one byte of free memory where the answer will be stored, and tell
the structure how much memory we gave it (io_Length). We can now give our order
to the Gameport Device by calling the DoIO() function.
The DoIO() function will return as soon as the Gameport Device has done our
request. We should now be able to find the answer at that memory position we
specified, and if it is not equal to GPCT_NOCONTROLLER some other task is
already using the port.
Here is an example:
/* Put the answer in this variable: */
BYTE type;
/* Create message port, allocate IOStdReq structure and */
/* open the Gameport Device. */
/* We want to know what controller, if any, is used: */
game_io_msg->io_Command = GPD_ASKCTYPE;
/* The reply should only be one byte long: */
game_io_msg->io_Length = 1;
/* Where we want the data stored: */
game_io_msg->io_Data = (APTR) &type;
/* Do our request, and return when it is done: */
DoIO( game_io_msg );
/* Check if some other task is already using the gameport: */
if( type)
{
/* YES! Some other task is already using this port! */
/* Lets check what type of controller he is using: */
switch( type )
{
case GPCT_MOUSE:
printf( "A mouse is connected to the port!\n" );
break;
case GPCT_RELJOYSTICK:
printf( "A proportional joystick is connected to the port!\n" );
break;
case GPCT_ABSJOYSTICK:
printf( "A normal joystick is connected to the port!\n" );
break;
}
/* We may now NOT close the device! If we do it, the other */
/* task will then not be able to use the gameport device */
/* either! We should only deallocate the StdIOReq structure */
/* and close the message port! */
/* Deallocate I/O request block: */
DeleteStdIO( game_io_msg );
/* Close message port: */
DeletePort( game_msg_port );
exit( ERROR );
}
else
{
/* OK! No other task is using the port! */
}
3.3.5 SET TYPE OF CONTROLLER
We should now tell the Gameport Device what type of controller (mouse or
joystick) we want to monitor. To do this set the io_Command field of the
IOStdReq structure to GPD_SETCTYPE. We must also give the field io_Data a
pointer to at least one byte of free memory where our request (mouse -
GPD_MOUSE or joystick- GPD_ABSJOYSTICK) is stored, and set the io_Length to 1
(we send 1 byte).
We can now give our order to the Gameport Device by calling the DoIO()
function.
/* We want to monitor mouse events: */
BYTE type = GPCT_MOUSE;
/* We want to set controller type: */
game_io_msg->io_Command = GPD_SETCTYPE;
/* The message is only one byte long: */
game_io_msg->io_Length = 1;
/* The data we want to send: */
game_io_msg->io_Data = (APTR) &type;
/* Do our request, and return when it is done: */
DoIO( game_io_msg );
When your program terminates it must set controller type back to
GPCT_NOCONTROLLER, before you close the Gameport Device! Do as above except
that you set type to GPCT_NOCONTROLLER.
/* We want to clear the port: */
BYTE type = GPCT_NOCONTROLLER;
/* We want to set controller type: */
game_io_msg->io_Command = GPD_SETCTYPE;
/* The message is only one byte long: */
game_io_msg->io_Length = 1;
/* The data we want to send: */
game_io_msg->io_Data = (APTR) &type;
/* Do our request, and return when it is done: */
DoIO( game_io_msg );
/* You may now close the Gameport Device, deallocate the */
/* IOStdReq structure and delete the message port. */
3.3.6 SET TRIGGER
No almost everything is prepared. The last thing you have to do is to tell the
Gameport Device when you want to receive gameport events. There exist five
different types of event that may occur if you want, and they are:
1. A button was pressed. (GPTF_DOWNKEYS)
2. A button was released. (GPTF_UPKEYS)
3. Timeout. Nothing has happened within a specified timelimit.
4. Delta X movements. (Mouse or stick moved to the right/left.)
5. Delta Y movements. (Mouse or stick moved up/down.)
You set what type of trigger you want by initializing the GamePortTrigger
structure (defined in the headerfile "devices/gameport.h"). The GamePortTrigger
structure look like this:
struct GamePortTrigger
{
UWORD gpt_Keys;
UWORD gpt_Timeout;
UWORD gpt_XDelta;
UWORD gpt_YDelta;
};
gpt_Keys:
If you want to trigger an event when a button is pressed, set flag
GPTF_DOWNKEYS. If you want to trigger an event when a button is released, set
flag GPTF_UPKEYS. If you want to trigger an event both when a button is pressed
as well as when it is released, set both flags with the binary operator | in
between, GPTF_DOWNKEYS|GPTF_UPKEYS.
gpt_Timeout:
If this value is not zero, a timeout message will be triggered if nothing has
happened during the specified timeperiod. The value is in vertical blank units
which occurs 60 times a second. So if you want a message to be triggered after
30 seconds, set gpt_Timeout to 30 * 60 = 1800.
gpt_XDelta:
How many x counts should occur before a message is broadcasted. If you are
monitoring a normal joystick, set this value to 1.
gpt_YDelta:
How many y counts should occur before a message is broadcasted. If you are
monitoring a normal joystick, set this value to 1.
Fill this GamePortTrigger structure with your requirements, and give the
IOStdReq structure a pointer to it. Set the io_Command field to GPD_SETTRIGGER
(we want to tell the system what should trigger an event). Finally do not
forget that the field io_Length should now be set to: sizeof( struct
GamePortTrigger).
Here is an example:
/* This structure will be filled with our requirements: */
struct GamePortTrigger gpt;
/* Set our requirements: */
gpt.gpt_Keys = GPTF_DOWNKEYS|GPTF_UPKEYS; /* Up or down. */
gpt.gpt_Timeout = 600; /* 10 seconds. */
gpt.gpt_XDelta = 5; /* At least 5 */
gpt.gpt_YDelta = 5; /* counts. */
/* We want to set controller trigger: */
game_io_msg->io_Command = GPD_SETTRIGGER;
/* The message is sizeof(struct GamePortTrigger) bytes long: */
game_io_msg->io_Length = sizeof( gpt );
/* Pointer to the data we want to send: */
game_io_msg->io_Data = (APTR) &gpt;
/* Do our request and return when it is done: */
DoIO( game_io_msg );
3.3.7 PREPARE TO READ
The last thing we have to do is to prepare the Gameport Device to send us
messages each time a gameport event is triggered. All information about the
gameport event is automatically stored in an InputEvent structure (defined in
the headerfile "devices/inputevent.h"), so what we have to do is to give the
Gameport Device a pointer to an InputEvent structure, specify the size to
sizeof( struct InputEvent), and set the io_command to GPD_READEVENT.
Here is an example:
/* Store the information about the gameport event */
/* in this structure: */
struct InputEvent data;
/* We want to read gameport events: */
game_io_msg->io_Command = GPD_READEVENT;
/* The gameport event is sizeof(struct InputEvent) bytes long: */
game_io_msg->io_Length = sizeof( data );
/* Where we want the data to be placed: */
game_io_msg->io_Data = (APTR) &data;
/* Do not use quick io: */
game_io_msg->io_Flags = 0;
3.4 HOW TO MONITOR THE GAMEPORT
Once the Gameport Device has been properly prepared, you can start monitoring
the port. Each time you want to check the port you send your request with help
of the function SendIO().
/* Do our request, and return without delay: */
SendIO( game_io_msg );
You will now receive a message at our message port as soon as a gameport event
is triggered (the user has moved the joystick, or a button has been pressed for
example). If we do not want to do anything while we are waiting for a gameport
event, it is best to call the WaitPort() function. It will put our task to
sleep so we do not waste any computer time. However, once there appear a
message at our port, our task is woken up.
/* Wait for a message to arrive at our message port. */
/* While we are waiting our task is put to sleep, */
/* which means that we will not waste any computer */
/* time. Zzz Zzz Zzz... */
WaitPort( game_msg_port );
Once we believe there is a message at our port, we can collect it with help of
the GetMsg() function. If we could not collect a message, GetMsg() will return
NULL. When we have collected a message we know that the Gameport Device has
filled our InputEvent structure with some interesting information about the
gameport event.
It is important to remember that from that moment we send our request with
SendIO() until we collect a message with GetMsg() we are not allowed to use the
InputEvent structure. However, once we have collected a message we may examine
it or change it as much as we want, until we send another request with
SendIO().
/* Try to Collect a message: */
if( GetMsg( game_msg_port ) )
{
/* If we have successfully collected a message, */
/* we may examine the InputEvent structure. */
}
PLEASE NOTE:
1. Before you can wait for a gameport event you must have sent a gameport
request by calling the SendIO() function.
2. Before your program terminates you must have collected every gameport
request you have sent! If you close the message port before you have answered
all events, something, not so nice, will happen.
3.4.1 THE INPUTEVENT STRUCTURE
As we said above, once you receive a gameport event you should examine the
InputEvent structure. The structure look like this: (Defined in the headerfile
"devices/inputevent.h".)
struct InputEvent
{
struct InputEvent *ie_NextEvent;
UBYTE ie_Class;
UBYTE ie_SubClass;
UWORD ie_Code;
UWORD ie_Qualifier;
union
{
struct
{
WORD ie_x;
WORD ie_y;
} ie_xy;
APTR ie_addr;
} ie_position;
struct timeval ie_TimeStamp;
};
/* These constants (together with many more) are also */
/* declared in the headerfile: */
#define ie_X ie_position.ie_xy.ie_x
#define ie_Y ie_position.ie_xy.ie_y
#define ie_EventAddress ie_position.ie_addr
ie_NextEvent:
Pointer to the next InputEvent structure. (All events are stored
chronologically.)
ie_Class:
What type of message it is. Not currently used by the Gameport Device.
ie_SubClass:
Extra field, sometimes used to more precisely explain what has happened. Not
currently used by the Gameport Device.
ie_Code:
This field tells us if a button has been pressed, or released. (Remember that
you will only receive such messages if you have set the gameport event triggers
GPTF_DOWNKEYS and/or GPTF_UPKEYS.)
If you are monitoring joystick events, and the user presses the fire button,
this field is set to IECODE_LBUTTON. If the user later on releases the fire
button, this field is set to IECODE_LBUTTON + IECODE_UP_PREFIX.
If you are monitoring mouse events, this field can contain one of the following
six different types of messages:
IECODE_LBUTTON: Left mouse button pressed.
IECODE_MBUTTON: Middle mouse button pressed.(*)
IECODE_RBUTTON: Right mouse button pressed.
IECODE_LBUTTON+IECODE_UP_PREFIX: Left mouse button was released.
IECODE_MBUTTON+IECODE_UP_PREFIX: Middle mouse button was released. (*)
IECODE_RBUTTON+IECODE_UP_PREFIX: Right mouse button was released.
[(*) Does not exist on the standard Amiga mouse.]
If no button was pressed or released, this field is set to IECODE_NOBUTTON.
ie_Qualifier:
Extra information for the code field. Not used by the Gameport Device.
ie_x:
Delta x movements.
If you are monitoring joystick events, 1 means that the stick was move to the
right, and -1 means that the stick was moved to the left.
If you are monitoring mouse events, this fields tells you how many x counts the
mouse has moved.
ie_y:
Delta y movements.
If you are monitoring joystick events, 1 means that the stick was move back,
and -1 means that the stick was moved forward.
If you are monitoring mouse events, this fields tells you how many y counts the
mouse has moved.
ie_addr:
The address of the device that sent the message.
ie_TimeStamp:
System timer.
3.4.1 COLLECT JOYSTICK EVENTS
Here is an example on how to collect joystick events: ("data" is a pointer to
an initialized InputEvent structure.)
/* Store the direction of the stick and */
/* button position in these variables: */
WORD xdirection;
WORD ydirection;
UWORD code;
/* Collect data: */
xdirection = data->ie_X;
ydirection = data->ie_Y;
code = data->ie_Code;
/* Was the fire button pressed or released? */
if( code == IECODE_LBUTTON )
printf("Button pressed. ");
if( code == IECODE_LBUTTON + IECODE_UP_PREFIX )
printf("Button released. ");
/* What is the position of the stick: */
switch(ydirection)
{
case -1:
printf( "Forward" );
break;
case 1:
printf( "Back" );
break;
}
switch(xdirection)
{
case -1:
printf( "Left" );
break;
case 1:
printf( "Right" );
break;
}
3.4.2 COLLECT MOUSE EVENTS
Here is an example on how to collect mouse events: ("data" is a pointer to an
initialized InputEvent structure.)
WORD x;
WORD y;
UWORD code;
/* Collect data: */
x = data->ie_X;
y = data->ie_Y;
code = data->ie_Code;
/* Check if an mouse button has been pressed/released: */
switch( code )
{
case IECODE_LBUTTON:
printf( "Left mouse button pressed" );
break;
case IECODE_MBUTTON:
printf( "Middle mouse button pressed" );
break;
case IECODE_RBUTTON:
printf( "Right mouse button pressed" );
break;
case IECODE_LBUTTON + IECODE_UP_PREFIX:
printf( "Left mouse button released" );
break;
case IECODE_MBUTTON + IECODE_UP_PREFIX:
printf( "Middle mouse button released" );
break;
case IECODE_RBUTTON + IECODE_UP_PREFIX:
printf( "Right mouse button released" );
break;
case IECODE_NOBUTTON:
printf( "No buttons pressed/released" );
/* This meant that you have either received a timeout */
/* message, or the mouse has been moved. */
break;
}
/* Check delta movement: */
printf( "Delta X: %4d Delta Y: %4d\n", x, y );
3.5 FUNCTIONS
Note that several of these functions are rather complicated, and have not been
fully explained in this chapter. For moreinformation se chapter XXXXX. Here is
at least a short description of what they are doing, and how they should be
used.
CreatePort()
This function allocates and initializes a MsgPort structure. If you give it a
string as first parameter it will also make the port public. If CreatePort() of
some reason could not create a message port it returns NULL, otherwise if
everything is OK it returns a pointer to the new MsgPort structure.
Note that a port that has been created by calling CreatePort(), must be
deallocated before your program terminates. The easiest way to deallocate a
message port is to use the DeletePort() function.
Synopsis:
msgp = CreatePort( name, pri );
msgp:
(struct MsgPort *) Pointer to the new MsgPort structure, or NULL if something
went wrong.
name:
(char *) Pointer to a string containing the name of the message port, or NULL.
If it is a string the port will be made public (so other tasks can find it)
else only our task can use it.
msgp:
(struct MsgPort *) Pointer to the MsgPort structure that should be deallocated.
pri:
(BYTE) This message port's priority. Usually set to 0 - normal priority.
DeletePort()
This function deletes a message port. Every message port that has been
allocated by CreatePort() must be deleted before the program terminates.
Synopsis:
DeletePort( msgp );
msgp:
(struct MsgPort *) Pointer to the MsgPort structure, that should be
deallocated.
CreateStdIO()
This function allocates and initializes a new IOStdReq structure. As only
parameter you give it a pointer to a message port that will be used by the
system to communicate with you. If the function succeeds, it returns a pointer
to the new IoStdReq structure, else NULL which means something went wrong.
Note that a IOStdReq structure that has been allocated by CreateStdIO(), must
be deallocated before the program terminates. To do this, use the function
DeleteStdIO().
Synopsis:
io = CreateStdIO( msgp );
io:
(struct IOStdReq *) Pointer to the new IOStdReq structure, or NULL if something
went wrong.
msgp:
(struct MsgPort *) Pointer to a MsgPort structure with which the system can
communicate with us.
DeleteStdIO()
This function deallocates a IOStdReq structure that has been created by
CreateStdIO(). Note that all allocated structures must be deleted before the
program may terminate.
Synopsis:
DeleteStdIO( io );
io:
(struct IOStdReq *) Pointer to the a IOStdReq structure that should be
deallocated.
OpenDevice()
This function will try to open the specified device. It will return 0 if
everything went OK, else an error number is returned.
Note that if you have opened a device, your program must close it before it
terminates. However, if you have opened a device that is already being used by
some other task you should not close it. Close a device by calling the
CloseDevice() function.
Synopsis:
error = OpenDevice( name, unit, io, flags );
error:
(long) OpenDevice() will return 0 if everything went OK, or an error number if
something went wrong.
name:
(char *) Pointer to a string containing the name of the device you want to
open. The name of the Gameport Device is "gameport.device".
unit:
(long) Which unit should be used. The Gameport Device can handle two ports:
unit 0 - the left "mouse port", port 1.
unit 1 - the right "joystick port", port 2.
io:
(struct IORequest *) Pointer to an already initialized IORequest or IOStdReq
structure.
flags:
(long) Additional information, set to 0.
CloseDevice()
This function closes a device that has previously been opened by an
OpenDevice() call. Note that if you have opened a device, your program must
close it before it terminates. However, if you have opened a device that is
already being used by some other task you should not close it.
Synopsis:
CloseDevice( io );
io:
(struct IORequest *) Pointer to an IORequest or IOStdReq structure that has
been used as parameter in the OpenDevice() function call.
3.6 EXAMPLES
Example1
This example demonstrates how to open the Gameport Device, and monitor Joystick
events. While we are waiting we put our task to sleep so we do not waste any
computer time.
Example2
Same as example 1, but instead of putting the task to sleep while we are
waiting for something to happen, we constantly try to receive joystick events.
This should for example be used in games. I am sure that you so not want that
all aliens should stop attacking the world just because the user has not moved
the stick.
Example3
This example demonstrates how to open the Gameport Device, and monitor mouse
events. While we are waiting we put our task to sleep so we do not waste any
computer time.
Example4
Same as example 3, but instead of putting the task to sleep while we are
waiting for something to happen, we constantly try to receive mouse events.
AUDIO DEVICE
4.1 INTRODUCTION
The Amiga was designed to be the first home computer which combined state of
the art graphics and impressive sound capabilities. Although more than six
years have passed since the first Amiga was released, very few other computers
can compete with the sound quality and flexibility offered by the Amiga.
In this chapter we look at how you can use the sound system to produce sound
effects as well as compose melodies. Since the sound system has been built into
the rest of the operating system, it will work smoothly together with other
processes, and can without problems be used by several tasks at the same time.
4.1.1 SOUND
Sound is simply vibrating air that is registered by our ears and interpreted as
lovely music, speech, white noise, etc. Although the simplicity it is possible
to produce almost unlimited number of variations of even a simple tune. Even a
single note sound very different if it was made by a piano or violin. By
altering the volume and rate it is played it is possible to produce even more
variations.
To better understand what sound is and how it is produced we will first look at
some commonly used technical words.
The "amplitude" of sound is the height of one of the waveforms. The higher
amplitude the louder will the waveform sound.
The "frequency" is the number of waveforms produced each second. The higher
frequency the higher will the tone be.
There exist a third sound variable called "timbre". The problem with it is that
it is very hard to describe, but is still very important for how the sound will
be perceived. It can be called "the sound identity". A violin and a piano can
play a tone with the same amplitude and frequency, but it will still sound very
different. Even two violins can sound very different.
The problem with reproducing the timbre is that by altering the frequency or
amplitude just a little (does not even need to be a noticeable change) it can
completely change the timbre. If you have read about "chaos theories" the
timbre can be described with the "butterfly effect"; even a small change in
wind by a butterfly can tip the balance and in the end cause a hurricane on the
other side of the world.
By combining several frequencies each with its own amplitude and timbre we can
produce unlimited numbers of variations.
4.1.2 DIFFERENT WAVEFORMS
Although each waveform from a note is different from another we usually do not
bother too much about it. It is usually enough to use just a few different
elementary waveforms. By altering the frequency and amplitude of these
waveforms we can then still produce millions of different sounds.
The most commonly used waveforms are the sine, triangular and square waveforms.
See illustration "Waveforms".
4.1.3 DIGITAL AND ANALOG WAVEFORMS
When we are using computers we can only handle digital numbers. To convert
these digital numbers into sound we must first convert the digital numbers to
analog values which can then be sent to a loud speaker. See illustration
"AnalogDigital".
On each Amiga computer there exist four "sound channels" which each can convert
digital numbers into analogue values. We can therefore play four different
sounds at the same time, which means that very complex sounds can be produced.
Two of the sound channels are connected to the right audio port, and the other
two are connected to the left audio port. We can therefore produce stereo sound
on the Amiga.
The sound channels are sometimes called "eight-bit channels" because they
convert eight-bit values into analog values. The Amiga can therefore produce
sounds were the values in the waveforms ranges from -128 to 127. Compact Disk
(CD) players use 16 bits and can therefore use even more values. For computers
today eight bits are usually more than enough, and can produce very impressive
sounds.
4.1.4 PLAY SAMPLED SOUNDS OR CREATE YOUR OWN TUNES
On the Amiga you can create your own tunes by using a waveform and modulating
it (changing the amplitude and frequency). You can also play sound data that
has been sampled (converted from analog sound to digital data). By using
sampled data you can reproduce very complex waveforms, and it will sound
natural.
To load and use already sampled data can very easily be done with help of the
two utilities I have included in the "Sound" manual, "Easy Sound" and "Include
Sound".
4.2 PREPARE THE AUDIO DEVICE
4.2.1 PRIORITY
Since several programs may be running at the same time it may happen that two
or more programs tries to use the same sound channel at the same time. To solve
problems like this we have
to use priorities.
If a program tries to use a sound channel that some other program is already
using two things may happen. If the new program has the same or lower priority
than the other program the new one is refused any access to the sound channel
and the old program may continue to use it. On the other hand, if the new
program has higher priority it will "steal" the sound channel from the old
program.
Some sounds are more important than others. An emergency alarm is of course
much more important than a background tune, and thus the emergency alarm should
have higher priority. Commodore has suggested the following priorities:
Type of sound Suggested priority
--------------------------------------------
Unstoppable sound 127
Emergencies 95
Attention 85
Speech 75
Information 60
Music 0
Sound effects -35
Background music -90
Silence -128
If a sound is so important that no one what ever happens should be able to
interrupt it we should use the "unstoppable sound priority". You should never
start a sound with this priority. You should instead start it with a lower (for
example with the "emergency") priority, and then increase it.
The "emergency" priority should be used when something happens that is so
important that the user must immediately know about it. Examples: the program
is going to visit the guru if the user does not immediately do some thing.
The "attention" priority is used to alert the user that something has happen
and the user must as soon as possible know about it.
The "speech" priority is used by the "Narrator Device" when it is producing
synthesized speech. Spoken information is usually more important for the user
than a nice tune.
The "information" priority should be used when you want to tell the user
something, but the user must not immediately know about it. This priority can
be used when you want to tell the user to read something, or start to input
some value etc...
When you are plying music you should use the "music" priority. This is for
programs that are using music as one of the primary functions. Background tunes
should use the "background priority".
The lowest priority level is "silent", and should actually never be used. If
you do not want to play the sound you should free the sound channel instead.
The priority can be varied inside each priority group. For example; soft and
quiet instruments/notes should have lower priority than loud and characteristic
instruments/notes. You can also alter the priority while the sound is being
played. The first part of an emergency sound can for example have higher
priority than the rest of the sound.
Sadly there does not exist any header file that defines the different
priorities. In my examples I will therefore use my own constants which are
declared like this:
Type of sound Priority
---------------------------
SOUND_UNSTOPPABLE 127
SOUND_EMERGENCIES 95
SOUND_ATTENTION 85
SOUND_SPEECH 75
SOUND_INFORMATION 60
SOUND_MUSIC 0
SOUND_EFFECT -35
SOUND_BACKGROUND -90
SOUND_SILENCE -128
4.2.2 ALLOCATING CHANNELS
There exist four sound channels which each one can convert digital sound data
into analogue sound which is sent to a loudspeaker. The first and last sound
channel is connected to the left audio port, while the second and third is
connected to the right audio port.
When you are reserving a sound channel you specify which channels you want to
use by using a value where the first bit represents the first sound channel,
the second bit the second sound channel and so on...
Bit: 3 2 1 0
-------------------------------------
Value: 8 4 2 1
Channel: 3 2 1 0
Audio port: Left Right Right Left
To reserve the first and last channel (both using the left sound channel) you
would use the value 9 (1001[bin] = 9[dec]). To reserve the second and third
channel (both using the right sound channel) you would use the value 6
(0110[bin] = 6[dec]). To reserve all four sound channels set the value to 15
(1111[bin] = 15[dec]).
Since other programs may be running at the same time it may happen that they
are already using one or more of the sound channels. If they have lower
priority than your program, you will "steal" the sound channel from them.
However, if you have the same or lower priority, you are refused any access to
that sound channel.
The good thing is that you may use an array of allocation values, and if the
first value could not be satisfied the audio device will automatically try the
next value. If you are lucky there exist a combination of sound channels which
is not currently being used.
If you want to play some sounds in stereo you have to use one sound channel
from the right and one from the left port. There exist four possible
combinations which satisfy this, and the "allocation array" should therefore
look like this:
Dec Bin Description
---------------------------------------------------
3 0011 First left and first right channel.
5 0101 First left and second right channel.
10 1010 Second left and first right channel.
12 1100 Second left and second right channel.
UBYTE allocation_array = { 3, 5, 10,12 };
The audio device will first try all combinations in the allocation array before
it will use the priority to "steal"
the channels for you.
4.2.3 CREATE WAVEFORMS
The audio device can play any type of waveform that is ranging from -128 up to
127. Usually you will use the normal sine, triangle, and square wave forms, but
you can equally well play more complex waveforms and even directly sample
sound.
It is important to note that the audio device is a part of the special custom
chips in the Amiga and can therefore only access sound data which is located in
the "chip memory". The data must also start on a word boundary, and consists of
an even number of bytes.
The more data used for the waveform the more complex waves can be used. The
smallest waveforms consists of only two values.
Example: We will create a sin waveform with 16 values.
/* If we are using mathematical functions */
/* like sin() we must include this file: */
#include <math.h>
/* Define a constant for the number of values in */
/* the wave form. It can then easily be changed. */
#define SINE_DATA_LENGTH 16
/* Declare a pointer to our sine wave: */
BYTE *sine_wave;
/* ... */
/* Allocate some chip memory for the sine wave: */
/* (All memory allocated by AllocMem() will */
/* always start on a word boundary.) */
sine_wave = (BYTE *) AllocMem( SINE_DATA_LENGTH, MEMF_CHIP );
/* Have we got the memory? */
if( !sine_wave )
clean_up( "Not enough memory!" );
/* Initialize the sine waveform: */
for( loop = 0; loop < SINE_DATA_LENGTH; loop++ )
sine_wave[ loop ] =
127 * sin( loop * 2 * PI / SINE_DATA_LENGTH );
4.2.4 NOTES AND FREQUENCIES
On octave consists of 12 notes. Here is a list of the frequencies that
represents the notes which are one octave higher than the middle octave on a
piano:
Note Frequency
---------------
A 880.0
A# 932.3
B 987.8
C 1046.5
C# 1108.7
D 1174.7
D# 1244.5
E 1318.5
F 1396.9
F# 1480.0
G 1568.0
G# 1661.2
To change octave you simply double/half these values. When you double the
values you go one octave up, and when you half the values you go one octave
down. Example, A = 880, one octave lower A = 440, one octave higher A = 1760,
two octaves higher and A = 3520.
When the audio device should play a waveform you do not specify which frequency
should be used. Instead you specify the "period" value which should be used for
each sample (value) in the waveform. The period can be calculated with the
following formula:
Period = Clock ticks per second / Frequency / Number of samples
The speed of the clock depends on if you have an European (PAL) or American
(NTSC) Amiga. On an NTSC machine the clock ticks 3579545 times per second,
while on a PAL machine it only ticks 3546895 times per second.
Example: We want to play the sine waveform we previously made with the
frequency 880 (note A). (We are using a European PAL Amiga.)
period = 3546895 / 880 / SINE_DATA_LENGTH;
Some extra information: If you have not already noticed it you may get the
wrong answer when you are dividing values with each other. The problem is that
C is always using the most accurate value in the formula as the base on which
it does the calculations. If you multiply an integer value and a float value
the C compiler will do the intermediate calculations with float values. If you
on the other hand multiply two integers with each other, the C compiler will do
the intermediate calculations with integer values.
The problem is that if you divide two values with each other, and both are
integers, the calculations will be preformed with integer values. Since you do
not have any decimals you can end up with a completely wrong answer. With
multiplications (and addition and subtraction as well) this will never cause
any errors.
When you are dividing two values with each other you should therefore always
convert the value (which you divide the other value with) to a float (or double
if necessary). Simply use normal casting. The rule is to always put the word
"(float)" in front of the bottom value when you are using division. In our
example the formula should look like this: (The second float is actually
unnecessary, but it does not matter.)
period = 3546895 / (float) 880 / (float) SINE_DATA_LENGTH;
4.2.5 THE AUDIO REQUEST BLOCK
The audio device is controlled like any other device with help of request
blocks. When you are using the audio device you should use the extended
"IOAudio" structure which is declared in the header file "devices/audio.h" as
this:
struct IOAudio
{
struct IORequest ioa_Request;
WORD ioa_AllocKey;
UBYTE *ioa_Data;
ULONG ioa_Length;
UWORD ioa_Period;
UWORD ioa_Volume;
UWORD ioa_Cycles;
struct Message ioa_WriteMsg;
};
ioa_Request:
The top part of the request block consists as always of an IORequest structure.
See below for more information about this structure.
ioa_AllocKey:
When you reserve a sound channel this field is given an unique "key" value.
Each time you pass the request to the channel this value will be compared with
the channel's current key value. If these two values are not the same, the
channel has been stolen by some other program with higher priority, and your
command is returned with the AUDIO_NOALLOCATION flag set.
If you are going to use another request block, and not the one you used to
allocate the channel with, you have to copy this key value to the other request
block.
ioa_Data:
Pointer to the waveform data. Note that the data must be in chip memory, and
start on a word boundary.
ioa_Length:
If you are going to play a waveform this filed should be given the length
(number of samples/values in the wave data).
ioa_Period:
This field should be given a period value. The period value is the number of
clock ticks that should be used for each sample (value in the waveform). See
above for more information about how to calculate the period value.
ioa_Volume:
The volume, which should be set to any value between 0 and 64. The maximum
value is 64, and the minimum value is 0 (silent). Many programs are always
using the maximum volume, but if you use different volume for different
situations you will gain a lot of "depth".
Imagine an adventure game where most of the sound effects are played rather
softly. The user will then use a higher volume on his/her own stereo (or
whatever the sound ports are connected to) to compensate for the low volume. No
imagine the feeling when a monster appear and you at the same time suddenly
increase the volume to maximum!
Sadly very few programs are using this very effective technique.
ioa_Cycles:
Before you play a waveform you should set this field to the number of times the
waveform should be played. If you set this field to 0 the waveform will
continuously be played until you aborts the command.
ioa_WriteMsg:
If you have set the flag "ADIO_WRITEMESSAGE" in the "io_Flag" field, and this
request will start a sound, this message will be sent when the sound starts to
be played. Remember to give the "mn_ReplyPort" field of the Message structure a
pointer to a message port to which the message should be sent.
The IORequest structure (which is a part of the IOAudiostructure) is defined in
the "exec/io.h" header file like this:
struct IORequest
{
struct Message io_Message;
struct Device *io_Device;
struct Unit *io_Unit;
UWORD io_Command;
UBYTE io_Flags;
BYTE io_Error;
};
io_Message:
The top part consists of a Message structure which will be sent to us when the
request has been completed (successfully or not).
The Message structure is defined in the header file "exec/ports.h" like this:
struct Message
{
struct Node mn_Node;
struct MsgPort *mn_ReplyPort;
UWORD mn_Length;
};
mn_Node:
It is only one field of the Node structure that should be used, and it is the
"ln_Pri" filed, which should be given this sound's priority value.
mn_ReplyPort:
This field should be given a pointer to the message port the reply should be
sent to.
mn_Length:
This value can be ignored.
io_Device:
This field will automatically be initialized when you use the request block
together with an OpenDevice() function call. It is simply a pointer to the
device which this request block is made for.
io_Unit:
This field will automatically be initialized when you reserve an audio channel
for this request block.
io_Command:
All commands should be set here. The following commands are accepted by the
audio device, and may be used: (Will be explained below.)
The unique audio device commands:
ADCMD_FREE
Free a sound channel.
ADCMD_SETPREC
Set priority.
ADCMD_FINISH
Stop playing the sound.
ADCMD_PERVOL
Set period and volume.
ADCMD_LOCK
Lock a sound channel.
ADCMD_WAITCYCLE
Wait for the cycle to finish.
ADCMD_ALLOCATE
Reserve a sound channel.
The normal commands which are accepted by most of the devices:
CMD_WRITE
Play the waveform.
CMD_READ
Get a pointer to current req.
CMD_STOP
Stop and do not start any sound.
CMD_START
Start to play sound again.
CMD_FLUSH
Remove all queued requests.
CMD_RESET
Reset the audio device.
io_Flags:
The following flags may be used:
IOF_QUICK If this flag is set no messages will be sent to the reply
port. This will speed up the execution, and can be useful if you have to use a
lot of request blocks.
ADIOF_PERVOL When you start to play a sound you can either use the volume
and period values which were used last time, or you may use new values. If you
want to use new volume and period values you should set this flag, and also set
the new values in this request block. Otherwise the old values will be used.
ADIOF_SYNCCYCLE Set this flag if you want that the command should be
synchronized with the wave form. If this flag is set, any modifications will
first occur when the whole waveform (cycle) have been played.
ADIOF_NOWAIT When you try to reserve a sound channel (with
ADCMD_ALLOCATE) and the audio device can not find any channel for you, it will
continue to try until it succeeds. The request will firs be returned when the
request has successfully been completed.
If you do not want to wait for the request to be completed you can set this
flag. If the audio device can not find any sound channel it will immediately
return the request with the "ADIOERR_ALLOCFAILED" error flag set.
ADIOF_WRITEMESSAGE Normally you will only receive a message at your reply port
when a request has been completed. It may however sometimes be necessary to
know when an audio request have started.
If you set this flag the message at the end of the request ("ioa_WriteMsg")
will be sent. Remember to give the message structure a pointer to a reply port
if you are using this flag.
io_Error:
If the request can not successfully be executed by the audio device, it will be
returned with one of the following error flags:
ADIOERR_NOALLOCATION When you reserve a sound channel you are given a "key"
value (set in the "ioa_AllocKey" field). Every time you send a request this key
value will be compared to the sound channel's current key value. If these two
values do not match the channel have been "stolen" by some other program. The
request will then be returned with this error flag set.
Note that you do not have to free a sound channel that has been stolen from
you.
ADIOERR_ALLOCFAILED When you try to reserve a sound channel it may happen
that all desired channels are occupied. If the audio device can not find a
channel it will normally continue to try until it found one. However, if you
have set the "ADIOF_NOWAIT" flag the request will immediately be returned if no
sound channels could be allocated, and this error flag is then set.
Remember to always check if you have received the channel or not if you are
using the "ADIOF_NOWAIT" flag.
ADIOERR_CHANNELSTOLEN If you have "locked" a channel, and another program tries
to steal it, the lock request will be returned to you with this flag set.
If the lock request is returned to you with this flag set you should as fast as
possible clean up after you and unlock the channel. It is first when you free
the locked channel the other program gets it.
NOTE! You must free the channel as soon as possible! The other program has
higher priority, but since you have locked the channel it can not get it.
4.2.6 OPEN THE AUDIO DEVICE
The audio device is very similar to other devices. You open a message port
through which the audio device can reply to you. You then allocate the request
block(s), and finally open the device.
1. Open a message port. Since it is only our task and the audio device that
will use the message port, we do not need to make it "public", therefore no
name. Priority should as usual be set to 0, normal priority. (Do not mix up
this priority for the message port with the priority for the sound channel,
which will later be set in the request block.)
struct MsgPort *replymp;
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
2. Allocate a request block of type IOAudio structure. The IOAudio structure is
an extended version of the normal request block, and should therefore be
allocated with help of the CreateExtIO() function with the size set to sizeof(
struct IOAudio ).
struct IOAudio *audio_req;
audio_req = (struct IOAudio *)
CreateExtIO( replymp, sizeof( struct IOAudio ) );
if( !audio_req )
clean_up( "Not enough memory!" );
3. Once the message port and the request block have successfully been created
you can "open" (gain access to) the audio device.
error = OpenDevice( AUDIONAME, 0, audio_req, 0 );
if( error )
clean_up( "Could not open the Audio Device!" );
If you want you can reserve sound channels at the same time as you open the
device. You must then:
(a) Set the sound priority. The top part of the request block, the IORequest
structure (ioa_Request) contains a Message structure ("io_Message") which
contains a Node structure (mn_Node) which finally contains a "ln_Pri" field.
(Piece of cake.) It is in this field you set the sound priority.
(b) Give the "ioa_Data" field a pointer to an array were the desired channel
combinations are listed. This array of desired channels are usually called "the
allocation array".
(c) Set the length of the allocation array in the "ioa_Length" field.
If the "ioa_Length" is not zero when you try to open the device, the audio
device will also try to reserve the desired channel(s). If the audio device can
not find any sound channels free, it will immediately (regardless if you have
set the "ADIOF_NOWAIT" flag or not) return with the "ADIOERR_ALLOCFAILED" error
message.
4.2.7 RESERVE CHANNELS
If you have not reserved any channel(s) when you opened the audio device, or
you want to reserve even more channels, you should use the "ADCMD_ALLOCATE"
command. The follwing things must be done:
1. Set the "ADCMD_ALLOCATE" flag in the "io_Command" field of the IORequest
structure.
audio_req->ioa_Request.io_Command = ADCMD_ALLOCATE;
2. Set the sound priority. The top part of the request block, the IORequest
structure (ioa_Request) contains a Message structure ("io_Message") which
contains a Node structure (mn_Node) which finally contains a "ln_Pri" field. It
is in this field you set the sound priority. Be careful so you set the correct
sound priority.
audio_req->ioa_Request.io_Message.mn_Node.ln_Pri = SOUND_EFFECT;
3. Normally will the request first be returned when the audio device has
successfully reserved the sound channel(s). If you set the "ADIOF_NOWAIT" flag
in the "io_Flags" field of the IORequest structure, the request will
immediately be returned, successfully or not. You can examine the "io_Error"
field of the IORequest structure to see if the request was successful or not.
If no channels could be reserved (allocated) the "ADIOERR_ALLOCFAILED" error
flag is set.
audio_req->ioa_Request.io_Flags = ADIOF_NOWAIT;
4. Give the "ioa_Data" field a pointer to an array were the desired channel
combinations are listed.
audio_req->ioa_Data = allocation_array;
5. Set the length of the allocation array in the "ioa_Length" field.
audio_req->ioa_Length = sizeof( allocation_array );
6. The audio request block has now been properly initialized, and it should now
be sent to the audio device. Normally you use the DoIO() and SendIO() commands,
but they may not be used when you are allocating channels. Instead you must use
the low level function BeginIO(), which is very similar to SendIO().
The reason why you may not use the DoIO() and SendIO() is that they will erase
some parts of the request block that may not be altered when you are using the
audio device.
Since BeginIO() is an asynchronous command, it will immediately return the
control to the program, you have to wait for the request to be executed. Simply
use the WaitIO() function which will put our program to sleep while we are
waiting.
WaitIO() will return 0 when the request has successfully been executed, else a
non zero value is returned.
BeginIO( audio_req );
error = WaitIO( audio_req );
if( error )
clean_up( "No channels!" );
After you have successfully allocated a channel you can check which channel(s)
you received by looking at the "io_Unit" field of the IORequest structure.
(Note that normally the "io_Unit" field of a IORequest structure contains a
pointer to a Unit structure. However, when you are using the audio device this
field is used to store an integer value, where the first four bits are used to
identify which channels are reserved. As before, bit zero represents channel 0
(left), bit one channel 1 (right), bit two channel 2 (right) and finally bit
three represents channel 3 (left).
channel = (UBYTE) audio_req->ioa_Request.io_Unit;
if( channel & 1 )
printf( "First left channel!\n" );
if( channel & 2 )
printf( "First right channel!\n" );
if( channel & 4 )
printf( "Second right channel!\n" );
if( channel & 8 )
printf( "Second left channel!\n" );
4.2.8 LOCK CHANNELS
After you have reserved a channel you may start to use it. When you reserved
the sound channel was the "ioa_AllocKey" given an unique key number. This key
number will be compared with the sound channels current key number, and if they
do not match, the channel(s) has/have been stolen and your request is returned
with the error flag "ADIOERR_NOALLOCATION" set.
If you want to use several request blocks for the same channel(s) you must
therefore copy the key value to all request blocks.
When you are using the request block to send commands the key value will always
be checked before any change is made. It can however sometimes be necessary to
skip the request block and directly modify the hardware registers. Using
request blocks take some time, and if you have to make changes several times
each second it may not be fast enough. You must then directly modify the sound
registers.
Normally you are not allowed to hit the hardware like this, but when you are
using the audio device you are allowed to do it. Of course, if you can manage
with the request blocks you should use them.
If you are using the hardware registers you may get into trouble if you are not
careful. If some other program has stolen the channel(s) from you and you use
the hardware registers, you will not receive any error messages. You can now
end up with modifying the other programs sounds, and this is not to be
recommended!
Before you may use the hardware registers you must therefore "lock" the
channel(s). To lock a channel you have to use a separate request block. This
request block is allocated and pre-initialized as usual with help of the
CreateExtIO() function: (We can of course use a different message port if you
want to separate all lock messages, but it is usually easier to use only one
message port for all audio messages.)
struct IOAudio *audio_lock;
audio_lock = (struct IOAudio *)
CreateExtIO( replymp, sizeof( struct IOAudio ) );
if( !audio_lock )
clean_up( "Not enough memory!" );
The lock should then have the following fields set:
1. The "ADCMD_LOCK" flag in the io_Command field of the IORequest structure
must be set.
audio_lock->ioa_Request.io_Command = ADCMD_LOCK;
2. The lock must be linked to the already opened audio device. The address to
the device can be found in the "io_Device" field of the IORequest structure.
audio_lock->ioa_Request.io_Device = audio_req->ioa_Request.io_Device;
3. You must tell the device which channels you want to lock. Normally you will
try to lock the channels you previously have reserved, and thus you can find
the channel value in the "io_Unit" field of the IORequest block.
audio_lock->ioa_Request.io_Unit = audio_req->ioa_Request.io_Unit;
4. Since you are going to use a new request block for some channels you have
previously reserved with another request block, you must copy the key value:
audio_lock->ioa_AllocKey = audio_req->ioa_AllocKey;
Once the lock request has been initialized it should be send by calling the
BeginIO() function.
BeginIO( audio_lock );
Once you have lock the channel(s) the request block will first be sent back
when some other programs tries to steal the sound channel(s) or when you free
the channel(s). If some other program wants the channel(s) and has higher
priority, the lock will be returned with the error flag "ADIOERR_CHANNELSTOLEN"
set. The channel has not yet been stolen, but you MUST immediately clean up all
necessary things and free the channel(s) as soon as possible so the other
program can get them. You may NOT keep the channels once the lock has been
returned!
While you are cleaning up and freeing the channels the other program is put to
sleep and does not know what is happening. If you are not fast enough the other
program will continue to "sleep" although it has higher priority and should
therefore immediately get the channels if you have not locked them. You must
therefore free the channels as fast as possible.
You may of course not modify the hardware registers any more after you have
deallocated the audio channel(s).
You can use the function CheckIO() to see if the lock has been sent back. See
below for more information about how to free a sound channel.
/* Has the lock been sent back? */
if( CheckIO( audio_lock ) )
{
/* Another programs what our channel(s)! */
/* Clean up. Reset any hardware */
/* registers etc, if necessary. */
/* Free the channel: */
audio_req->ioa_Request.io_Command = ADCMD_FREE;
/* Send the request: (We may use the DoIO() function */
/* when we deallocate a channel.) */
DoIO( audio_req );
}
4.3 USE THE AUDIO DEVICE
Once you have declared a request block and successfully opened the audio device
you may start to use it. When you are playing sounds you may either use the
request block to issue the sound commands, or you can directly modify the
hardware registers. The first method is cleaner, but the second method is much
faster and may sometimes be necessary if you intend to issue many commands.
4.3.1 PLAY SOUNDS
To play some sound you have to do the following things:
1. Give the "ioa_Data" field of the request block a pointer to a waveform you
want to play. (The waveform data must be located in chip memory, and start on
an even byte address.)
audio_req->ioa_Data = sine_wave;
2. Set the length of the waveform in the "ioa_Length" field. (Must be an even
number of bytes.)
audio_req->ioa_Length = SINE_DATA_LENGTH;
3. Set the number of times (cycles) the waveform should be played in the
"ioa_Cycles" field. If you want to play the waveform continuously until you
abort the request you should set the field to 0.
audio_req->ioa_Cycles = 3;
4. Set the command "CMD_WRITE" flag in the "io_Command" field which tells the
device that you want to play a tune.
audio_req->ioa_Request.io_Command = CMD_WRITE;
5. If you want to use a specific volume and period values you must set the
"ADIOF_PERVOL" flag in the "io_Flags" field. The values in the "ioa_Volume"
and "ioa_Period" fields will then be used, else the previous volume and period
values will be used.
audio_req->ioa_Request.io_Flags = ADIOF_PERVOL;
audio_req->ioa_Volume = 32;
audio_req->ioa_Period = 3546895 / 880 / SINE_DATA_LENGTH;
Once all desired values have been set you send the request to the audio device,
by calling the BeginIO() function. If you want to wait for the sound to be
completed you can use the WaitIO() function. Note that you should NEVER try to
wait for a request that has not been started, or if the "ioa_Cycles" field has
been set to 0 (forever). If the request already has been completed the WaitIO()
function will immediately return.
BeginIO( audio_req );
error = WaitIO( audio_req );
if( error )
clean_up( "Error while playing the sound!" );
4.3.2 USE SEVERAL REQUEST BLOCKS
Remember that you may not modify the request block once it has been sent, by
calling BeginIO(), and have not yet been completed. Once the request has been
completed you of course start to modify it again. If you want to play several
notes at the same times in different sound channels, you may therefore have to
use several request blocks.
If you want to use some of the audio commands that is changing the sound which
is currently being played you also have to a use separate request blocks. In
many programs you will see four request blocks where each one handles one audio
channel. Very often there also exist one or more extra request blocks that are
used to modify sounds which are currently being played.
If you have reserved a sound channel with a request block you can not use
another request block to play sounds on it if you do not first copy some
important values to the new request block.
When you reserve a channel the request block is given a unique key value, and
if this key value does not match with the sound channel's current key value the
request will fail. Before you may use a new request block you must therefore
first copy the "ioa_AllocKey" value to the new request.
You must also copy the pointer to the audio device from the old request to the
new one. The unit number and all stuff in the Message structure must also be
copied. In the end, it is actually easier to copy the whole request block, even
if some fields does not have to be copied.
Here is a small demonstration on how to copy all values in a request block to a
new one. The "audio_req" is a pointer to an already initialized audio request
block, where "change_req" is a new and uninitialized request block.
/* Source and destination pointers: */
BYTE *first_ptr;
BYTE *second_ptr;
/* Allocate request blocks, open audio device, */
/* reserve channels, etc... */
/* Copy the first request block to the */
/* second one: (byte by byte) */
/* Get the start addresses of both request blocks: */
first_ptr = (BYTE *) audio_req;
second_ptr = (BYTE *) change_req;
/* Copy byte by byte: */
for( loop = 0; loop < sizeof( struct IOAudio ); loop++ )
{
/* Copy: */
*second_ptr = *first_ptr;
/* Next byte: */
first_ptr++;
second_ptr++;
}
4.3.2 PLAY DOUBLE BUFFERED SOUNDS
If you play several sounds after each other you will hear a "click" between the
sounds. The reason why you hear this click is that once a sound has been
completed it takes some time for your program to initialize a new request and
send it to the audio device. Although this delay is very short it is long
enough to be noticed, especially if several programs are running at the same
time.
Luckily there is a solution for this problem. Before the first sound has been
complete you should send the next sound request. When the first sound has
stopped the second sound is already available for the audio device, and it will
immediately be played. Because of this, there is such a short interrupt between
the sounds that you will not hear any annoying clicks.
This technique of sending the next sound when the first one is still being
played is usually called "double buffering", and can in many ways be compared
with "double buffered displays" which is described in the manual "Graphics",
chapter "Graphical Tricks".
1. Prepare the first request block and send it to the audio device.
2. Prepare the second request block, and send it to the audio device.
3. Wait for the first request block to be completed. When the request block is
returned you prepare it again for a new sound, and send it to the audio device.
4. Wait for the second request block to be completed. When the request block is
returned you prepare it again for a new sound, and send it to the audio device.
5. Jump back to step 3 until all sounds have been played.
6. Wait for both requests to be completed, and quit.
4.3.3 MODIFY THE HARDWARE REGISTERS
Usually you should never try to modify hardware registers directly but instead
use special functions that does it for you. However, when you are using the
audio device you may alter the hardware registers of the sound channels.
Modifying hardware registers directly is much faster than using functions that
does it for you, and when playing sounds speed can sometimes be crucial.
Before you may alter any of the sound registers you must first lock the
channel(s)! When you modify hardware registers no one is no checking that you
really own the channel(s). If you have reserved a channel and not locked it,
some other program may steal it and you would not notice that. You could then
end up altering the other programs sound channel(s), which is rather unpolite
not to say dangerous.
To use the hardware registers you have to declare an external array of
"AudChannel" structures. The AudChannel structure is defined in the Custom
structure which is declared in the header file "hardware/custom.h". The whole
Custom structure is automatically initialized by the Amiga. The AudChannel
array, which must be called "aud" is also initialized.
The AudChannel structure is defined like this: (Declared in header file
"hardware/custom.h")
struct AudChannel
{
UWORD *ac_ptr;
UWORD ac_len;
UWORD ac_per;
UWORD ac_vol;
UWORD ac_dat;
UWORD ac_pad[2];
} aud[4];
ac_ptr:
Pointer to the waveform data.
ac_len:
The length of the waveform.
ac_per:
The period value.
ac_vol:
The volume.
ac_dat:
This field contains the first two bytes which will be played. The DMA channels
will automatically move data from the waveform to this field, two bytes each
time, when needed. You can alter these values directly, but this will take
longer time than letting the DMA fetch the sound automatically, so it is not
recommended.
ac_pad[2]:
Unused areas, should not be used.
When you declare the "aud" array you probably have to use the keyword "far"
since the hardware data is most certainly far away from your program. Remember
that the aud array has been declared and initialized by the Amiga itself, and
you should therefore declare it as an external array. Example:
extern struct AudChannel far aud[];
To use the hardware audio channels you must also declare an external UWORD
which must be called "dmacon". This variable is also automatically initialized
by the Amiga. Example:
extern UWORD far dmacon;
This "dmacon" variable controls the DMA channels. A DMA channel can be
described as a data cable where data can be sent without disturbing the main
processor. It is because of the DMA (DMA = "Direct Memory Access") you can play
sounds at the same time as your program can use the main processor to do
something else.
It is this variable which controls all "DMA writing", and must therefore be
used when you want to play a sound. To play a sound on one of the audio
channels you have to give it a value where the leftmost bit is set (the
DMAF_SETCLR constant) and the bit which correspond to the audio channel you
want to start playing. Bit one represents the first audio channel, bit two the
second audio channel and so on. You should however not use the direct values
but instead the constants which are declared in the "hardware/dmabits.h" header
file.
DMAF_SETCLR
This flag should always be set together with the channel(s) you want to be
played.
DMAF_AUD0
First left audio channel.
DMAF_AUD1
First right audio channel.
DMAF_AUD2
Second right audio channel.
DMAF_AUD3
Second left audio channel.
If you want to use several audio channels you simple set all desired channel
flags separated with the binary OR operator "|". To start all audio channels do
like this: (You must of course first have reserved and locked them before you
may do this!)
dmacon = DMAF_SETCLR|DMAF_AUD0|DMAF_AUD1|DMAF_AUD2|DMAF_AUD3;
Since you can now directly alter the volume, period, and waveform by directly
writing to the hardware the changes will be very simple and quick. Just
remember to now and then check if your lock has been returned, and if so clean
up after you and free the channel(s) as soon as possible. Do not be greedy and
ignore the lock, too many programs do that which is very irritating!
4.4 CLEAN UP AFTERWARDS
When your program terminates you have to clean up after yourself, as usual. It
is important that you do not forget to do this since other programs are denied
any access to the audio channels and memory you forget to deallocate. Remember
that your programs must also be able to clean up after itself if it suddenly
has to terminate because of some error.
4.4.1 UNLOCK CHANNELS
All audio channels that you have reserved must be given back to the system
before your program may terminate. If you have locked a channel and another
programs wants it, you must return it as soon as possible. However, if you have
not locked the channel and another program has stolen it you do not have to
free it.
To free one or more audio channels you simply send a request with the
"ADCMD_FREE" command flag set. All channels which are selected in the "io_Unit"
field of the request block will be resetted and given back to the system. Any
locks on the channels will be removed.
If some other program has stolen the channel(s) from you the request will be
returned with the error message "ADIOERR_NOALLOCATION". You do not have to do
anything with channel(s) which has/have been stolen from you. It is the other
program's duty to clean it/them up.
When you want to free a channel you may use the DoIO() function, instead of
BeginIO(). Normally you should only use the BeginIO() function when working
with the audio device, but when you free channel(s) the DoIO() will work
equally well.
audio_req->ioa_Request.io_Command = ADCMD_FREE;
DoIO( audio_req );
4.4.2 REMOVE ALL MESSAGES AND CLOSE THE REPLY PORT
You must close all message ports you have opened. Just remember to remove all
messages before you close it. A short while loop that removes the messages
until it can not find any more is usually enough.
while( GetMsg( replymp ) )
printf( "Collected a message at the reply port.\n" );
DeletePort( replymp);
4.4.3 CLOSE THE DEVICE
The audio device itself must of course also be closed if you have opened it. If
you have open it several times with different request blocks you have to close
all of them.
CloseDevice( audio_req );
4.4.4 DEALLOCATE THE REQUEST BLOCKS
All audio request blocks you have created must be deallocated with help of the
DeleteExtIO() function. The size should be set to the size of an IOAudio
structure. Just remember to close the device before you deallocate the request
block.
DeleteExtIO( audio_req, sizeof( struct IOAudio ) );
4.4.5 DEALLOCATE SOUND BUFFERS
If you have allocated a sound buffer to store the waveform in you must also
deallocate it. Remember to deallocate the same amount of memory as when you
allocated it.
FreeMem( square_wave, SINE_DATA_LENGTH );
4.5 AUDIO DEVICE COMMANDS
The audio device is controlled by set of commands. We have looked at the
ADCMD_ALLOCATE, ADCMD_FREE, and CMD_WRITE commands. Most things can be managed
with these commands, but there exist a set of other commands that may sometimes
be needed. These new commands can be divided into two groups. The first group
consists of the general device commands which can be used on most devices. The
second group consists of a set special commands that only the audio device can
understand and handle.
4.5.1 GENERAL DEVICE COMMANDS
Here is the complete list of general device commands, together with a short
description:
CMD_WRITE
Start to play a sound on one channel.
CMD_READ
Check which request is currently playing.
CMD_STOP
Temporarily stop all audio requests.
CMD_START
Restart the previously stopped audio requests.
CMD_FLUSH
Remove all queued requests.
CMD_RESET
Reset the audio device.
4.5.1.1 CMD_WRITE
The CMD_WRITE command is used when you want to start to play a sound on a
specific sound channel. Note that it can only start one sound channel.
This command has already been explained, so see previous sections for more
information.
4.5.1.2 CMD_READ
The CMD_READ command is used when you want to get the address of the request
which is currently being used. You specify a sound channel you want to check by
setting the corresponding bit in the io_Unit field. If a sound is currently
being played on that channel the "io_Data" field of our request block will
contain the address of the other request which is currently playing the sound.
If no sound is being played the "io_Data" field is set to NULL.
This command is useful if you have sent a lot of requests to the audio device
and you want to see which request is currently being played. Note that you must
copy the sound channels key value to your request before you can check it. The
key value can be found in the "ioa_AllocKey" field of the request which
reserved the channel. If your request block's "ioa_AllocKey" value does not
match the sound channel's current key value the ADIOERR_NOALLOCATION error
message is returned.
4.5.1.3 CMD_STOP
The CMD_STOP command will temporarily stop all requests for the specified
channel(s). All requests will be queued, and first processed when you later
restart the channel(s) by using the CMD_START command. The "io_Unit" field of
the request specifies which channels should be stopped. Remember to copy the
key value from the request block that reserved the sound channel(s). If your
request block's "ioa_AllocKey" value does not match the sound channel's current
key value the ADIOERR_NOALLOCATION error message is returned.
4.5.1.4 CMD_START
The CMD_START command should be used when you want to restart one or more
f\previously stopped sound channel(s). The "io_Unit" field of the request
specifies which channels that should be restarted. Remember to copy the key
value from the request block that reserved the sound channel(s). If your
request block's "ioa_AllocKey" value does not match the sound channel's current
key value the ADIOERR_NOALLOCATION error message is returned.
4.5.1.5 CMD_FLUSH
The CMD_FLUSH command will remove all currently queued commands from the
specified channels. Remember to copy the key value from the request block that
reserved the sound channel(s). If your request block's "ioa_AllocKey" value
does not match the sound channel's current key value the ADIOERR_NOALLOCATION
error message is returned.
4.5.1.6 CMD_RESET
The CMD_RESET command will reset all specified channels. All requests currently
queued will be removed, the audio hardware registers are cleared, and if the
channel has been stopped it is restarted. Remember to copy the key value from
the request block that reserved the sound channel(s). If your request block's
"ioa_AllocKey" value does not match the sound channel's current key value the
ADIOERR_NOALLOCATION error message is returned.
4.5.2 SPECIAL AUDIO DEVICE COMMANDS
Here is the complete list of special audio device commands. These commands can
only be used with the audio device.
ADCMD_ALLOCATE
Tries to reserve one or more audio channels.
ADCMD_FREE
Frees one or more audio channels.
ADCMD_SETPREC
Change the priority of a sound.
ADCMD_FINISH
Finish (aborts) a sound.
ADCMD_PERVOL
Change the volume and period of a sound.
ADCMD_LOCK
Will lock audio one or more channel(s).
ADCMD_WAITCYCLE
Waits for a cycle to be completed.
4.5.1.1 ADCMD_ALLOCATE
The ADCMD_ALLOCATE command will reserve one or more audio channels. This
command has already been discussed, so see previous sections for more
information about this command.
4.5.1.2 ADCMD_FREE
The ADCMD_FREE command will free one or more audio channels that has(have)
previously been stolen. This command has already been discussed, so see
previous sections for more information about this command.
4.5.1.3 ADCMD_SETPREC
The ADCMD_SETPREC command allows you to change the priority of a sound channel
that has previously been reserved. You may for example want to use a very high
priority at the beginning of the sound, and later decrease it. Remember to copy
the key value from the request block that reserved the sound channel(s). If
your request block's "ioa_AllocKey" value does not match the sound channel's
current key value the ADIOERR_NOALLOCATION error message is returned.
4.5.1.4 ADCMD_FINISH
The ADCMD_FINISH command will finish an already started sound. This may be
needed if you have set a very high cycle value, or you are playing a sound "for
ever" (the ioa_Cycle field set to zero.) This command has the same effect as
calling the AbortIO() function. Both options are equally good, but the
ADCMD_COMMAND looks a bit "cleaner" (at least some C programmers think so).
Remember to copy the key value from the request block that reserved the sound
channel(s). If your request block's "ioa_AllocKey" value does not match the
sound channel's current key value the ADIOERR_NOALLOCATION error message is
returned.
4.5.1.5 ADCMD_PERVOL
The ADCMD_PERVOL command can be useful if you want to change the period and/or
volume value(s) of a sound which is already playing. Just remember to set the
ADIOF_PERVOL flag in the "io_Flags" field, or else your command will have no
effect. Remember to also set the new volume and period values you want to be
used in the "ioa_Volume" and "ioa_Period" fields.
As always, remember to copy the key value from the request block that reserved
the sound channel(s). If your request block's "ioa_AllocKey" value does not
match the sound channel's current key value the ADIOERR_NOALLOCATION error
message is returned.
4.5.1.6 ADCMD_LOCK
The ADCMD_LOCK command is used to lock one or more channels. If some other
program has higher priority and tries to steal our channel(s), the lock is
returned to us. The channels are however still not stolen. When the lock is
returned we should as fast as possible clean up and free the channels so the
other program can get it.
See the previous sections for more information about this command.
4.5.1.7 ADCMD_WAITCYCLE
If you send a request with the "ADCMD_WAITCYCLE" command set, the request will
first be sent back when the sound which is currently being played has reached
the end of the cycle. If no sound is being played it will immediately return.
Remember to copy the key value from the request block that reserved the sound
channel(s). If your request block's "ioa_AllocKey" value does not match the
sound channel's current key value the ADIOERR_NOALLOCATION error message is
returned.
4.6 FUNCTIONS
BeginIO()
BeginIO() is a low level form of the SendIO() function. The advantage with
BeginIO() is that no fields of the request block will be altered as which is
the case with SendIO(). When you are using the audio device you should almost
always use the BeginIO() and not DoIO() or SendIO(). If you are freeing
channels you may however use these functions too.
Synopsis
:
BeginIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed. In this
case a pointer to an IOAudio structure.
DoIO()
DoIO() is used to send requests to a device, and waits for it to be completed.
While the program is waiting it is put to sleep so it will not waste any
computer time. DoIO() will return first when the request have been completed or
failed, and no message is therefore sent to the reply port. When you are using
the audio device you may only use this function when you free audio channel(s).
Synopsis
:
error = DoIO( req );
error:
(long) DoIO() will return first when the request has been completed or
something has failed. If the request was successfully completed zero is
returned, else an error number is returned. What error number depends on which
device was used.
req:
(struct IORequest *) Pointer to the request you want to have executed. In this
case a pointer to an IOAudio structure.
SendIO()
SendIO() is used to send requests to a device, but will return immediately
without any delay. To check if the request have been completed use the
CheckIO() function, or look at the request's reply port for any messages. Once
the request has been completed you must remove the message at the reply port.
(CheckIO() will not do it.) To remove a message use the function Remove(). Note
that you may NOT close the device before all requests have been completed or
aborted!
When you are using the audio device you may only use this function when you
free audio channel(s).
Synopsis
:
SendIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed. In this
case a pointer to an IOAudio structure.
CheckIO()
CheckIO() is used to check if a previously started request has been completed.
Note that this function will not remove the message at the reply port. This
must be done with the Remove() function.
Synopsis
:
ptr = CheckIO( req );
ptr:
(long) CheckIO() will either return NULL if the request have not been completed
or it will return a pointer to the request block.
req:
(struct IORequest *) Pointer to the request you want to check. In this case a
pointer to an IOAudio structure.
WaitIO()
WaitIO() will wait for the request to be completed, and while the program is
waiting it is put to sleep so no computer time is wasted.
Synopsis
:
error = WaitIO( req );
error:
(long) WaitIO() will return first when the request, that has previously been
sent, has been completed or something has failed. If the request was
successfully completed zero is returned, else an error number is returned. What
error number depends on which device was used.
req:
(struct IORequest *) Pointer to the request you want to wait for to be
completed. In this case a pointer to an IOAudio structure. Note that the
request must have already been sent to the device by either a SendIO() or
BeginIO() function call.
AbortIO()
AbortIO() will try to abort a previously started request. Instead of using this
function you can use the ADCMD_FINISH command as explained above.
A request that is aborted will have its io_Error field set to IOERR_ABORTED
(defined in header file "exec/errors.h").
Synopsis:
AbortIO( req )
req:
(struct IORequest *) Pointer to the request you want to abort. In this case a
pointer to an IOAudio structure.
CloseDevice()
CloseDevice() will close a device. Note that you should NOT close the device
before all started asynchronous requests have either been completed or aborted.
Synopsis:
CloseDevice( ioreq );
ioreg:
(struct IORequest *) Pointer to the device's request block. In this case a
pointer to an IOAudio structure.
OpenDevice()
OpenDevice() will try to open the specified device.
Synopsis:
error = OpenDevice( name, unit, req, flags );
error:
(long) If OpenDevice() managed to open the device it returns 0, else an error
number is returned. If you try to reserve one or more audio channels at the
same time, and there are not any available, the error message
"IOERR_ALLOCFAILED" is returned.
name:
(char *) Name of the device you want to open. The name of the audio device is
defined as AUDIONAME in header file "audio.device".
unit:
(long) Not used by the audio device.
req:
(struct IORequest *) Pointer to a request block. In this case a pointer to an
IOAudio structure.
flags:
(long) Not used by the Audio device.
4.7 EXAMPLES
Example 1
This program will play some notes (A to G#) with help of the Audio Device. It
will use one of the audio channels (the first one which is free).
Example 2
This program is very similar to the previous example, but this time are we
using double buffered sounds, and there will therefore not be any annoying
"clicks" between the notes.
The technique with double buffered sound is that while the first sound is
played the second sound is already sent to the audio device. When the first
sound terminates the second sound can immediately start without any delay.
While the second sound is being played the first sound is prepared and sent and
so on... Because there is never any delay between the sounds there will never
be any annoying clicks.
Example 3
This program demonstrates how you can play some sampled data. Remember that
sampled data is just a more complicated waveform.
The sampled sound has been converted into numbers by "PrintSound", a utility
included in the "Sound" manual.
Example 4
This program demonstrates how you can play a sound continuously. While the
sound is being played we slowly alter the period and volume values.
Example 5
This example demonstrates how you can play sounds in STEREO. First we play a
sound in the left channel, then we switch to the right, and then back again,
and so on...
In this example are we reserving the audio channels at the same time as we open
the audio device. We use two audio requests, one for the left channel and the
other one for the right channel.
Example 6
This program will play some notes (A to G#) with help of the Audio Device. It
will use as many audio channels as possible, and we are modifying the hardware
registers directly instead of using the special Audio Device commands.
You are allowed to use the hardware registers directly if you make sure that no
other task can steel them from you before you have cleared all necessary
registers.
NARRATOR DEVICE
5.1 INTRODUCTION
The Amiga can not only produce crisp clear four channel stereo sound, it can
even speak. The operating system was designed so this unique feature easily and
efficiently can be used. With very little effort you can let the Amiga read
information.
Sadly very few programs use this extremely useful feature. Although artificial
speech sounds a bit monotone and uninteresting it can be used in many
situations to transfer information from the computer to the user.
Imagine if the user has to check hundreds of different numbers on the screen
when the original numbers are on papers. This very boring and slow task can be
made much easier if the Amiga automatically reads the values and the user only
has to look at the paper.
5.2 ARTIFICIAL SPEECH
Imitating human speech is a very difficult process. Even the most sophisticated
super computers can still not manage to produce all the different intonations
and sounds as a real human voice can. The Amiga's synthesized speech mechanism
is not at all as powerful as what these super computers have, but compared to
other home computers, it is outstanding.
Producing artificial speech on the Amiga can be divided into two steps:
1. First we have to convert the text we want to be read into phonetical text.
Phonetical text is written as the words should sound and not as how the words
are spelled.
If you open a dictionary you will notice that there exist a specification on
how every word should be pronounced. The special symbols used to tell you how
the word should sound are usually called "the phonetic symbols", and are
defined in the beginning or end of the dictionary. The pronunciation of all
words can be constructed with help of these symbols.
On the Amiga we use the same technique. However, since the real phonetical
symbols are very strange and can not be typed with a normal keyboard, we use a
special "computerised" version of these symbols See next section for a complete
list of these phonetical symbols.
A great news is that you do not have to write the text with the phonetical
language. You can instead use a special function called Translate() which can
be found in a the "Translator Library". This function can automatically convert
english text into phonetics.
2. After you have created a string with the phonetical text you send it to the
"Narrator Device" which will transform the phonetical symbols into sound, and
the text is read.
5.2.1 PHONETIC SYMBOLS
The phonetical symbols used by the dictionaries can sadly not be used on normal
computers since they use very strange signs that can not be reproduced on
normal keyboards. Instead we have to use a special computerized set phonetical
symbols which is defined like this:
Sound Phoeme Example Type
---------------------------------
A AE Hat Vowel
AO Talk Vowel
EY Page Diphthong (A diphtong is a union
--------------------------------- of two vowel sounds or
B B Bad Consonant vowel letters.)
---------------------------------
C CH Chin Consonant
K Car Consonant
---------------------------------
D D Did Consonant
---------------------------------
E EH Ten Vowel
IY Feet Vowel
---------------------------------
F F Four Consonant
---------------------------------
G G Got Consonant
---------------------------------
H /H How Consonant
---------------------------------
I AY Five Diphthong
ER Fur Vowel
IH Sit Vowel
---------------------------------
J J Yes Consonant
---------------------------------
K K Cat Consonant
---------------------------------
L L Leg Consonant
---------------------------------
M M Man Consonant
---------------------------------
N N No Consonant
NX Sing Consonant
---------------------------------
O AA Got Vowel
AW Now Diphthong
AX About Vowel
IX Solid Vowel
OH Saw Vowel
OW Home Diphthong
OY Join Diphthong
UH Put Vowel
---------------------------------
P P Pen Consonant
---------------------------------
Q K Cat Consonant
---------------------------------
R R Red Consonant
---------------------------------
S S Sit Consonant
SH She Consonant
T DH Then Consonant
T Tea Consonant
TH Thin Consonant
---------------------------------
U AH Cup Vowel
UW Too Diphthong
---------------------------------
V V Voice Consonant
---------------------------------
W W Wet Consonant
---------------------------------
X /C Loch Consonant
---------------------------------
Y Y Yellow Consonant
---------------------------------
Z Z Zoo Consonant
ZH Vision Consonant
---------------------------------
For example, the word "hello" can phonetically be written as "/HEHLOW", and the
word "house" as "/HAWZ".
5.2.2 INTONATION
When you are speeking you do not use the same voice all the time. Some parts of
the words or sentences are stressed by using a higher voice, were other parts
are prounounced more softly.
For example, the letter "e" in the word "hello" is usually prounounced in a
higher voice. By stressing some words the text will sound more interesting, and
if used with care it can be a very effective way to get the user's attention.
Futhermore, by stressing different parts of a sentence the whole meaning of the
sentence can change quite dramatically. Take the sentence "I will go home" for
example. By stressing different words the user will interpretate the meaning
differently. With these four words we can actually get five rather different
meanings:
1. If no stressin is used, the meaning is simply that the person is going home.
2. By stressing the first word "I" it now sounds like the person him/her self
is going home but the other persons (if there are any) may do something
differently.
3. By stressing the second word "will" it now sounds like the persons is really
determined and that he/she really wants to go home.
4. If the person stresses the third word "go" he/she means that he/she will
walk home, although the word "go" can mean travelling by car, train etc.
5. Finally, if the last word is stressed the person wants to go to his/her
home, and not any other place.
To stress parts of a word you can put a single digit directly after the phonem
which should use a higher intonation. The digit 0 means no special intonation,
while the higher digit the more stressed will the phonem be. The digit 9 should
be used when you want maximum intonation.
For example, the stress the letter "e" in the word "hello" the phonetically
string should be written as "/HEH4LOW". The phonem "EH" will then be medium
stressed.
5.2.3 PUNCTATION AND SPECIAL SYMBOLS
There exist some symbols which are used to help the reader to use the righ
intonations. The most commonly used symbole is the puncation ".", but others
are the question mark "?", the comma ",", dash "-", etc... Most of these common
punctations are understod by the Amiga, and will automatically alter the
intonation and speed correctly.
Here are the symbols you may use:
Type
Symbol
How it affects the intonation
Punctation
.
This will make the intonation of the last word a bit softer, and there will be
a long pause before any following words are read.
Question mark
?
This will cause the Amiga to increase the intonation of the last word so it
sounds like a question, and it will be followed by long pause before any
following words are read.
Comma
,
The comma is used to divide a sentense into several parts, but putting a small
pause before the following text.
Dash
-
The dash can be used as the comma, and works much in the same way. There exist
a lot of confusing rules on when the dash and when the comma should be used,
but I do not intead to expalin it here. (Mainly because I do not know anything
about these rules, sadly. I use what looks best, which I think is an excellent
rule.)
Parentheses
( )
Parantheses are used to explain something which is not so important that it has
to be in the main sentense, but is can still be important for the reader. The
text inside parantheses starts off by using less intonation than the rest of
the sentense.
5.2.4 VOLUME
Although the intonation is altering the volumne a little, most of the text will
be read with the same volume. You can however tell the narrator device to use a
completely different volume, and this can be very effective. If you normally
use a soft voice, but suddenly increase it to the maximum the user will not
miss that part.
Producing the correct sound can take some time, but once you have mastered it,
the text will sound rather smooth and, although a bit computerized, rather
human.
5.3 CONVERT TEXT INTO PHONETIC SYMBOLS
Converting text into phonetical symbols does not have to be a hard task. If you
are going to read an English text (or American, Irish, Scottish etc...) you can
use the Translate() function. It will automatically convert the text into
(almost) correct phonetics which can be used.
The Translate() function is located in the "Translator" library which must be
opened before you can use the function. The Translate() function is actually
the only function in this library.
5.3.1 OPEN THE TRANSLATOR LIBRARY
To open the translator library you declare a global pointer to it which must be
called "TranslatorBase", and you then call the function OpenLibrary() to open
it.
Synopsis
:
library_ptr = OpenLibrary( name, revision );
library_ptr:
(struct Library *) If the function could open the library it returns a pointer
to it, else NULL is returned. The translator library is located on the system
disk, and is therefore loaded when needed. If some other program already has
opened it has already been loaded into memory and a pointer is immediately
returned to it.
Be careful to close the library before your program terminates. If you forget
it, the library will remain in the memory until the computer is switched off.
name:
(char *) Pointer to a string containing the name of the library you want to
open. When you open the translator library, the name should be
"translator.library".
revision:
(long) This value tells the Amiga which oldest version may be used. If there
exist a library with the same or higher value the function will successfully
open the library, else NULL is returned. If you simply want any version, set
this field to zero.
Here is an example: (Remember that the pointer to the translator library must
ALWAYS be called "TranslatorBase"!)
/* Pointer to the translator library: */
struct Library *TranslatorBase;
/* Open the library: */
TranslatorBase = OpenLibrary( "translator.library", 0 );
/* Have we successfully opened it? */
if( !TranslatorBase )
clean_up( "Could not open the translator library!" );
5.3.2 TRANSLATE TEXT
Once you have opened the translator device you may start to use the Translate()
function. This function will work best with English sentences, but can be used
for many other languages if you afterwards do some small changes of the
phonetic text.
Synopsis
:
char_left = Translate( in, in_len, out, out_len );
char_left:
(long) If not all phonetic text could fit in the out string the function will
automatically only translate the words which will fit. (It will not stop in the
middle of a word.) If all words could be translated zero is returned, else a
negative value is returned. This value tells us how many letters of the "in"
string have been translated. With this value we can then call the function
again and translate the remaining text.
Note that the number is negative.
in:
(char *) Pointer to the (English) text string which should be read.
in_len:
(long) The length of the (English) text string.
out:
(char *) Pointer to a string where the phonetic text string will be stored.
(The "in" string is converted and copied to the "out" string.)
out_len:
(long) The length of the phonetic (out) string.
Here is an example on how to translate a string. The nice thing is that we can
translate strings of any size. If the translated string does not fit in the
buffer, we divide it up into smaller parts.
/* Translated buffer size: */
#define PHONETIC_BUFFER_SIZE 50
/* Number of characters that were translated, or */
/* zero if all characters were translated: */
int char_translated;
/* This variable contains the current position */
/* in the string which is translated: */
int current_position;
/* The original string: */
char *original_string = "The Amiga C Encyclopedia!";
/* The phonetic string: */
char phonetic_string[ PHONETIC_BUFFER_SIZE ];
/* Open the translator library: */
TranslatorBase = (struct Library *)
OpenLibrary( "translator.library", 0 );
/* Have we successfully opened the library? */
if( !TranslatorBase )
clean_up( "Could not open the translator library!" );
/* Start with the first character in the original string: */
current_position = 0;
/* Translate (parts of) our string into phonetics: */
char_translated =
Translate( original_string,
strlen( original_string ),
phonetic_string,
PHONETIC_BUFFER_SIZE );
/* Print the translated part: */
printf( "%s\n", phonetic_string );
/* As long as Translate() does not return zero we stay */
/* in this while loop and continues to translate the */
/* original string into phonetics: */
while( char_translated )
{
/* Increase the current position in the original string: */
/* (Remember that "char_translated" variable is negative, */
/* and we must therefore use the "-=" operator and not */
/* the "+=" to increase the current position.[-- = +]) */
current_position -= char_translated;
/* Translate the following part our string into phonetics: */
/* (Note that when we put brackets after a string we we */
/* get the character at the specified position, but since */
/* we want the address of that position we also have to */
/* put the pointer "&" sign in front of the string.) */
char_translated =
Translate( &original_string[ current_position],
strlen( &original_string[ current_position] ),
phonetic_string,
PHONETIC_BUFFER_SIZE );
/* Print the translated part: */
printf( "%s\n", phonetic_string );
}
/* All words have now been translated! */
5.3.3 CLOSE THE TRANSLATOR LIBRARY
Before your program terminates it must close the translator library. This is
especially important with this library since it is loaded from the system disk
when opened, and if you do not close it a lot of memory is wasted. Be careful
about this.
As I have said many times, make sure that your program does not only terminate
nicely when it has reached the end. It must also manage to terminate nicely if
it suddenly has to quit because of
some error. If you look at the examples which accompanies this manual you will
see that most of them use a function called "clean_up()". The idea with this
function is that it examines all resources, and if it finds out that something
has been opened or allocated it closes it before the program terminates.
Because it examines all the resources before it attempts to close or deallocate
them the function can always be called and it will clean up everything, even if
you only have opened and allocated some resources. Try to use something similar
in your own programs.
5.4 READ PHONETIC SYMBOLS
The translator library and the function Translate() are only used to convert
normal (English) text into phonetical strings. These phonetical strings can
then be read by the Amiga, but to do this you have to use another resource on
the Amiga which is the famous "Narrator Device". (Tataaa, trumpets and drums!)
The narrator device is very straight forward and easy to use. As always you use
request blocks to send your commands, and the device will reply by sending
messages to a reply (message) port. The device should of course first be opened
by calling the OpenDevice() function. When your program wants to terminate the
device and message port are closed and the request block is deallocated.
5.4.1 NARRATOR REQUEST BLOCK
When you are using the narrator device you have to use an extended request
block called "narrator_rb" which is defined like this: (Defined in header file
"devices/narrator.h".)
struct narrator_rb
{
struct IOStdReq message;
UWORD rate;
UWORD pitch;
UWORD mode;
UWORD sex;
UBYTE *ch_masks;
UWORD nm_masks;
UWORD volume;
UWORD sampfreq;
UBYTE mouths;
UBYTE chanmask;
UBYTE numchan;
UBYTE pad;
};
message:
The top part of this request block consists of the standard input output
request block. This IOStdReq block is explained below.
rate:
The speaking rate in words per minute. The default rate (DEFRATE) has been
defined as 150 words per minute. The maximum speed (MAXRATE) is 400 words and
the minimum speed (MINRATE) is 40 words per minute.
pitch:
The voice's pitch. This is the medium pitch value. Stressed words are spoken
with a higher pitch, while soft words are spoken with a lower pitch. By
changing this value the whole voice is affected.
The default pitch (DEFPITCH) is 110. The maximum pitch (MAXPITCH) is 320 and
the minimum pitch is 65.
mode:
This value will affect the way the text is read. If you set the flag
"NATURALF0" the voice will go up and down as the text is read. On the other
hand, if you set the flag "ROBOTICF0" the voice will be very dull and does not
change as the text is read.
sex:
If the flag "MALE" is set a dark male voice will be used. If you instead set
the flag "FEMALE" a lighter and sharper voice will be used. (Do not ask me why
they did not define it as "BITCH".)
*ch_masks:
This field should be given a pointer to an "allocation array" which specifies
which audio channels that should be used.
There exist four sound channels which can be used. The first and last sound
channel is connected to the left audio port, while the second and third is
connected to the right audio port.
To specify which channels you want to use you use a value where the first bit
represents the first sound channel, the second bit the second sound channel and
so on...
Bit: 3 2 1 0
-------------------------------------
Value: 8 4 2 1
Channel: 3 2 1 0
Audio port: Left Right Right Left
To reserve the first and last channel (both using the left sound channel) you
would use the value 9 (1001[bin] = 9[dec]). To reserve the second and third
channel (both using the right sound channel) you would use the value 6
(0110[bin] = 6[dec]). To reserve all four sound channels set the value to 15
(1111[bin] = 15[dec]).
The allocation array should consist of all the channel combinations you like.
The narrator device will first try to reserve the channels specified by the
first value. If it could not get these channels, it tries the next values until
if finds a combination it could reserve or it reaches the end of the allocation
array and the operation fails.
For example. If you want that the text should be read in stereo you have to use
one audio channel from the right and one from the left port. There exist four
possible combinations which satisfy this, and the "allocation array" should
therefore look like this:
UBYTE allocation_array = { 3, 5, 10,12 };
Dec Bin Description
---------------------------------------------------
3 0011 First left and first right channel.
5 0101 First left and second right channel.
10 1010 Second left and first right channel.
12 1100 Second left and second right channel.
nm_masks:
This field should be given a number which tells the narrator device how many
values there are in the allocation array.
volume:
The volume of the voice. The default volume (DEFVOL) is the same as the maximum
volume (MAXVOL) which is defined as 64. The minimum volume (MINVOL) is 0 which
is the same thing as silent.
I myself does not like that the maximum volume is often used. It would be much
better if everyone were using the medium volume as the default value. The user
will then turn up the volume on his/her stereo to compensate for the lower
volume. The advantage is that if you now suddenly play a very loud sound or use
a very loud voice it will also be very loud.
Imagine a game which is normally using a sound level of 32. The user is sitting
alone in his/her own room, and it is very late. Imagine what would happen if a
monster would appear in the the game and at the same time the volume was
increased to maximum. It will undoubtedly be a nice (hmmm) surprise for the
user.
sampfreq:
This field specifies which frequency should be used. The default frequency
(DEFFREQ) is 22200. The maximum value (MAXFREQ) is 28000 and the minimum value
(MINFREQ) is 5000.
mouths:
Normally this field should be set to zero. However, of you are going to use the
"mouth" request, as explained below, you should set a non zero value.
chanmask:
This field contains the value which was used of the allocation array to reserve
the audio channel(s). This field is used by the system, but if you like you may
examine it, but you may never change it.
numchan:
Number of channels used. This field is also used by the system, but if you like
you may examine it, but you may never change it.
pad:
This field is not used, and should therefore never be used.
The IOStdReq structure (which is a part of the narrator_rb structure) is
defined in the "exec/io.h" header file like this: (Only some parts of this
structure will be used, so you do not have bother too much about it.)
struct IOStdReq
{
struct Message io_Message;
struct Device *io_Device;
struct Unit *io_Unit;
UWORD io_Command;
UBYTE io_Flags;
BYTE io_Error;
ULONG io_Actual;
ULONG io_Length;
APTR io_Data;
ULONG io_Offset;
};
io_Message:
The top part consists of a Message structure which will be sent to us when the
request has been completed (successfully or not). This message structure will
automatically be given a pointer to the reply port when the narrator_rb
structure is created, and the priority will automatically be set when used, so
we do not have to bother too much about it.
io_Device:
This field will automatically be initialized when you use this request block
together with an OpenDevice() function call. It is simply a pointer to the
device which this request block is made for.
io_Unit:
This field will automatically be initialized when you send the request block to
the narrator device.
io_Command:
The following commands are accepted by the narrator device, and may be used:
(Will be explained below.)
CMD_WRITE
Start to speak.
CMD_STOP
Stop all speech requests.
CMD_START
Start to speak again.
CMD_FLUSH
Remove all queued requests.
CMD_RESET
Reset the narrator device.
io_Flags:
No flags are used.
io_Error:
If the request can not successfully be executed by the narrator device, it will
be returned with one of the following error flags:
ND_NoMem
Not enough memory for the request.
ND_NoAudLib
Can not open the audio device.
ND_MakeBad
Can not execute the MakeLibrary() function.
ND_UnitErr
Wrong unit number (not 0).
ND_CantAlloc
Can not find any free audio channels to reserve.
ND_Unimpl
Not a valid command.
ND_NoWrite
Nothing is being read, so you can not read any "mouth" values.
ND_Expunged
Wrong expunge value.
ND_PhonErr
Wrong phonetic code.
ND_RateErr
Too large or small rate value
ND_PitchErr
Too large or small pitch value
ND_SexErr
Wrong sex number used.
ND_ModeErr
Wrong mode value used.
ND_FreqErr
Too high or low frequency value.
ND_VolErr
Too high or low volume value.
io_Actual:
Used by the system.
io_Length:
The number of characters in the phonetic string that should be read.
io_Data:
Pointer to the phonetic sting.
io_Offset:
Used by the system.
5.4.2 OPEN THE NARRATOR DEVICE
To open the narrator device does not differ from opening other devices. First
you have to create a message port to which the device can send messages to you.
Secondly you have to create one or more narrator_rb structures by using the
CreateExtIO() function. Finally you can open the narrator device with help of
one of the request blocks.
1. Open a message port. Since it is only our task and the narrator device that
will use the message port, we do not need to make it "public", therefore no
name. Priority should as usual be set to 0, normal priority.
struct MsgPort *replymp;
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
2. Allocate a request block of type narrator_rb structure. The narrator_rb
structure is an extended version of the normal request block, and should
therefore be allocated with help of the CreateExtIO() function with the size
set to sizeof( struct narrator_rb ).
struct narrator_rb *narrator_req;
narrator_req = (struct narrator_rb *)
CreateExtIO( replymp, sizeof( struct narrator_rb ) );
if( !narrator_req )
clean_up( "Not enough memory!" );
3. Once the message port and the request block have successfully been created
you can "open" (gain access to) the narrator device. The default values will
now also be set in the request block.
error = OpenDevice( "narrator.device", 0, narrator_req, 0 );
if( error )
clean_up( "Could not open the Narrator Device!" );
5.4.3 READ PHONETIC TEXT
Once you have successfully opened the narrator device you may start to use the
narrator device. The fields of the request block with which the narrator device
was opened with have automatically been given the default values:
1. The rate is set to 150 words per minute.
2. The pitch is set to 110 Hz.
3. The mode is set to normal (living) voice.
4. The sex field is set to man.
5. The sampfreq is set to 22200.
6. The volume is set to 64.
The following fields must be initialized by yourself:
1. You must give the "io_Data" field of the IOStdReq structure a pointer to the
string of phonetic words that should be read.
2. Give the "io_Length" field of the IOStdReq structure the number of
translated characters that should be read. You can use the function strlen() to
find out how long the translated string is.
3. You have to give the "ch_masks" field a pointer to the allocation array that
should be used.
4. Give the "nm_masks" field the number of values which exist in the allocation
array.
5. Finally you should give the "io_Command" field of the IOStdReq structure the
"CMD_WRITE" flag. (We are going to send [write] text to the narrator device.)
Here is an example:
/* The text should be read in stereo: */
UBYTE allocation_array[]=
{
LEFT0F|RIGHT0F, /* First left and first right channel. */
LEFT0F|RIGHT1F, /* First left and second right channel. */
LEFT1F|RIGHT0F, /* Second left and first right channel. */
LEFT1F|RIGHT1F /* Second left and second right channel. */
};
/* The phonetic string: */
char *phonetic_string = "/HEH4LOW";
/* Message port opened... */
/* "narrator_rb" created... */
/* Narrator device opened... */
/* Set our requirements: */
/* 1. Give it a pointer to the phonetic string: */
narrator_req->message.io_Data = (APTR) phonetic_string;
/* 2. Set the length of the phonetic string: */
narrator_req->message.io_Length = strlen( phonetic_string );
/* 3. Desired channel combinations: */
narrator_req->ch_masks = allocation_array;
/* 4. Size of the allocation array: */
narrator_req->nm_masks = sizeof( allocation_array );
/* 5. Send (write) the text to the device: */
narrator_req->message.io_Command = CMD_WRITE;
You may of course also change the other values if you do not want to use the
default ones.
After the request has been initialized can it be sent to the narrator device
which will read the phonetic string. To send request you can either use the
synchronous command DoIO() or the asynchronous command SendIO().
The DoIO() function will send the request to the device and put your program to
sleep. When the device has finished your request it is returned and your
program wakes up. If something failed will DoIO() return an error number and a
copy of their value will also be stored in the "io_Error" field of the request
block.
The SendIO() function should be used when you want to continue to do something
while the narrator device is reading the text. After you have called SendIO()
you can put your task to sleep by either using the WaitIO() or Wait() function.
If you want to check if the request has been completed you can use the
CheckIO() function.
All these functions have been described in chapter "Devices", and will
therefore not be repeated here.
An example on a synchronous call: (Your program is put to sleep while the
narrator device completes your request.)
BYTE error;
/* ... */
/* Read the text: */
error = DoIO( narrator_req );
/* Were there any errors? */
if( error )
clean_up( "Problems with reading the text!" );
An example on an asynchronous call: (Your program continues to run while the
text is read.)
/* Start to read: */
SendIO( narrator_req );
while( !CheckIO( narrator_req ) )
{
/* Do something... */
}
/* Collect message at the reply port... */
/* Were there any errors? */
if( narrator_req->message.io_Error )
clean_up( "Problems with reading the text!" );
5.4.4 USING SEVERAL REQUEST BLOCKS
If you have created several request blocks will only the one which you opened
the narrator device with be correctly preinitialized. These structures which
have not been initialized must be prepared before they may be used. The
simplest solution is actually to copy the whole structure, because you will
then be sure that no values are forgotten.
Here is an example:
/* Source and destination pointers: */
BYTE *first_ptr;
BYTE *second_ptr;
/* Allocate request blocks, open narrator device, etc... */
/* (The two request blocks are called "first_narrator_req" */
/* and "second_narrator_req".) */
/* Copy the first request block to the second one: */
/* Get the start addresses of both request blocks: */
first_ptr = (BYTE *) first_narrator_req;
second_ptr = (BYTE *) second_narrator_req;
/* Copy byte by byte: */
for( loop = 0; loop < sizeof( struct narrator_rb ); loop++ )
{
/* Copy: */
*second_ptr = *first_ptr;
/* Next byte: */
first_ptr++;
second_ptr++;
}
5.4.5 CLEAN UP
When your program terminates you have to clean up and free all allocated
resources. This is what has to be done:
1. Remove any messages still left at the message (reply) port.
2. Close the message (reply) port(s).
3. Close the narrator device.
4. Deallocate the request blocks.
5.4.5.1 REMOVE ALL MESSAGES
Before you close anything you should first remove all messages from the message
(reply) port. A short while loop that removes the messages until it can not
find any more is a simple but effective solution.
while( GetMsg( replymp ) )
printf( "Collected a message at the reply port.\n" );
5.4.5.2 CLOSE MESSAGE PORT
After all messages have been removed from the port you may close it by calling
the DeletePort() function. Note that you should never try to close a message
port you have not opened.
DeletePort( replymp);
5.4.5.3 CLOSE THE NARRATOR DEVICE
The narrator device itself must of course also be closed if you have opened it.
Note that you should never try to close a device you have not opened.
CloseDevice( narrator_req );
5.4.4 DEALLOCATE THE REQUEST BLOCKS
Finally you should deallocate all request blocks you have created. Use the
DeleteExtIO() function. The size should be set to the size of a narrator_rb
structure. Just remember to close the narrator device before you deallocate the
request block(s).
DeleteExtIO( narrator_req, sizeof( struct narrator_rb ) );
5.5 THE NARRATOR'S MOUTH
The narrator device offers a rather unique but sometimes useful feature. The
narrator device can be used to draw a mouth as the text is read. It may sound a
bit strange, but if you are using animations this can be very helpful.
The narrator device will tell us how wide and high the mouth should be, and
with this we can easily construct a simple mouth.
5.5.1 MOUTH REQUEST BLOCK
To use this "mouth" feature you have to use a special request block which is
called "mouth_rb", and looks like this: (Defined in header file
"devices/narrator.h".)
struct mouth_rb
{
struct narrator_rb voice;
UBYTE width;
UBYTE height;
UBYTE shape;
UBYTE pad;
};
voice:
The top part of the mouth block consists of a "narrator_rb" structure. When you
create this structure the message port will automatically be initialized. The
following fields must however be initialized by yourself:
1. You must copy the "io_Device" pointer from the request which is reading the
text.
2. The "io_Unit" number must also be copied.
3. The "io_Error" field should be set to zero.
4. Finally you should set the command "CMD_READ" in the "io_Command" field.
After you have sent this request block and it is returned you can examine the
"io_Error" field to check if there were any problems. See above for a complete
list of error flags.
When you are using this request, it will be returned to you if one of the
following thing has happened:
1. The size of the mouth has changed.
2. Something went wrong, and the "io_Error" field will contain an error flag.
If you have received the "ND_NoWrite" the Amiga has stopped talking, and you
should not try to draw any mouth any more.
width:
After you have successfully executed this request you can examine this field to
check how wide the mouth should be. The request block will only be returned
when the size of the mouth has changed, or the Amiga has stopped talking.
height:
This field will contain the height of the mouth.
shape:
This field may only be used by the system, and should never be changed.
pad:
No used, and should not be used.
5.5.2 CREATE A MOUTH REQUEST BLOCK
Since the size of the mouth_rb is larger than the standard sized request block
you have to use the CreateExtIO() function to allocate and preinitialize the
request block. Normally it is easiest to connect this structure to the same
message port as the other request blocks are connected to. You can of course
use a separate message port if you want.
struct mouth_rb *mouth_req;
mouth_req = (struct mouth_rb *)
CreateExtIO( replymp, sizeof( struct mouth_rb ) );
if( !mouth_req )
clean_up( "Not enough memory!" );
5.5.3 PREPARE THE MOUTH REQUEST BLOCK
After you have created the mouth_rb structure you have to copy some values, as
explained above, from the request block which will read the text. You must also
set some values yourself.
Here is an example on how to initilaize the mouth request:
/* Set the mouth width and height to zero: */
mouth_req->width = 0;
mouth_req->height = 0;
/* Give the mouth request a pointer to the narrator device: */
mouth_req->voice.message.io_Device = narrator_req->message.io_Device;
/* Give the mouth request the current unit number: */
mouth_req->voice.message.io_Unit = narrator_req->message.io_Unit;
/* No error number (so far): */
mouth_req->voice.message.io_Error = 0;
/* The mouth request should look at (read) the request */
/* which is currently talking: */
mouth_req->voice.message.io_Command = CMD_READ;
The mouth_rb request block can now be used.
5.5.4 GET THE SIZE OF THE MOUTH
The mouth request block should be sent to the narrator device after you have
sent a normal read request. If the device is currently not reading any text the
mouth request will immediately be returned with the error flag "ND_NoWrite"
set.
If the device is reading some text the mouth request will first be returned
when the mouth has changed size, or if the narrator device has reached the end
of the text string. If the device reached the end of the string and stops
talking the mouth request will be returned with the error flag "ND_NoWrite"
set. However, if the request is returned and no error flag is set, the mouth
has changed size and should be redrawn.
Since your program must be able to work while the narrator device is reading
the text you have to use the asynchronous command SendIO() to start the read
request. Here is an example on how to use the narrator's mouth:
/* TRUE as long the Amiga is reading text: */
BOOL still_talking;
/* ... */
/* Start to read: (The read request must be sent by */
/* the asynchronous command SendIO().) */
SendIO( narrator_req );
/* The Amiga is reading the text: */
still_talking = TRUE;
/* As long as the Amiga is reading the text */
/* we stay in the while loop: */
while( still_talking )
{
/* Send the mouth request to the narrator device. The request */
/* will be returned when the mouth width and/or height have */
/* changed, or the device has stopped reading the text: */
DoIO( mouth_req );
/* Has the device stopped reading the text: */
if( mouth_req->voice.message.io_Error == ND_NoWrite )
still_talking = FALSE;
else
{
/* No, the device is still reading. The mouth must have */
/* changed, so we better redraw it: */
/* Draw the new mouth... */
}
}
/* We know that the Amiga has stopped speaking, */
/* and hence we do not have to wait for the */
/* narrator to finish reading. We should however */
/* check if we have successfully read the text: */
if( narrator_req->message.io_Error )
clean_up( "Error while reading!" );
5.6 EXAMPLES
Example 1
This very simple example demonstrates how to open the translator library,
translate a string, and finally close the library before the program
terminates.
Example 2
This example demonstrates how you can use a while loop to translate parts of a
string until the whole string has been translated.
Example 3
This example demonstrates how to translate a string into a phonetical string
which is then read by the narrator device.
Example 4
This example very similar to the previous one, but this time are we using a
different voice. By altering the rate, pitch, mode, sex and volume, you can
produce very different sounds.
Example 5
This example demonstrates how you can let the Amiga read small stories. By
altering the rate, pitch, mode, sex and volume parameters it can sound like
several persons are talking. It can also be used to express emotions and stress
important parts of the text.
This example is using some home made functions which makes life a little bit
easier. If you have to read a lot of text I recommend you to use special
functions like these. It will then be much easier to write (and read) the
program code.
Example6
This example demonstrates how to use the mouth request block to draw a talking
mouth.
TRACKDISK DEVICE
6.1 INTRODUCTION
All Amiga models are delivered with at least one internal disk drive, but you
may connect up to three extra disk drives. The disk drivers are using normal
double sided double density (2DD) 3 1/2" disks. 5 1/4" disks may also be used,
but this is not supported by the trackdisk device.
The trackdisk device consists of a set of low level routines which are used by
other higher level processes like AmigaDOS. When you want to read and write
data you should normally only use AmigaDOS or the special file commands in C.
The trackdisk device should only be used when you want to write disk
viewers/editors, or disk-copy programs.
Although the disk routines are normally considered to be very slow on the
Amiga, it should be noted that it is actually not the disk drivers which are
slow. AmigaDOS is using a very flexible disk operating system and because of
this the disk drivers appears to be very slow. The low level routines are on
the other hand very quick and are turbocharged with the "Blitter" (special
coprocessor on the Amiga) which is specialized in moving large quantities of
data extremely fast.
To read and write so called "raw data" with the lowlevel routines is much
faster that to use the higher and more sophisticated AmigaDOS routines. Many
games use these low level routines to quickly load graphics and sound effects.
Normal programs should, as said before, not use the lowlevel routines described
here. However, it can still be interesting to read about it anyway, since it is
always good to know what is actually happening inside the Amiga.
6.2 AMIGA DISK DRIVERS
The trackdisk device was designed to handle normal double sided double density
(2DD) 3 1/2" disks (also called "1 megabyte disks"). The disk can be logically
(fysically too, but that is not recommended) be divided into several small data
areas where the actual data is stored.
Each data area, normally called "Sector", can store 512 bytes of data and 16
bytes of so called "label" data. The label area is used to identify the sector
and what is stored here, and can therefore not be used to store raw data.
A group of eleven sectors is called "Cylinder" or "Track", and there exist 80
cylinders on each side of the disk. In total you can store 512 (bytes/sector) *
11 (sectors/cylinder) * 80 (cylinders/side) * 2 (double sided disks) = 901120
bytes = 880KB (901120 / 1024).
The trackdisk device will only handle whole cylinders. Even if you only want to
read some sectors of a cylinder the complete cylinder will be loaded into the
trackdisk's own memory buffer. This technique of reading and writing complete
cylinders greatly improves the storage capacity and speed of the drives. The
actual reading and writing process is very quick compared to the time it takes
for the drive to move the head to the right position and start rotating the
disk.
Your program will not notice that the trackdisk device is only using complete
cylinders. If you want to read some sectors, the trackdisk device will read a
complete cylinder, but gives you only those sectors you wanted. If you later
want to read some sectors more it may happen that the device immediately can
give you the data you wanted without having to access the disk.
6.3 TRACKDISK DEVICE
The trackdisk device is controlled in the same manner as all other devices. You
send your commands to it with help of a request block, and the device will send
messages back to a specific reply port. So, to use the trackdisk device you
have to: (Same as with most other devices.)
1. Create a message port with which the device can send messages back to us.
See chapter 1 "Devices" for more information.
2. Allocate and preinitialize a request block.
3. Open the trackdisk device.
6.3.1 REQUESTBLOCK
A normal IOStdReq structure ("standard request block") is in most cases
possible to use, but if you want to use some special features described further
down you have to use the extended request block "IOExtTD". This IOExtTD
structure is declared in header file "devices/trackdisk.h" as:
struct IOExtTD
{
struct IOStdReq iotd_Req;
ULONG iotd_Count;
ULONG iotd_SecLabel;
};
iotd_Req:
The top part of the request block consists of a standard request block
(IOStdReq structure).
iotd_Count:
Each disk drive you are working with is assigned a counter value which is
increased each time a disk is removed. If you are using the "extended" commands
(described below) the trackdisk device will check the driver's count value with
this value. If the counter is greater than this value the request will be
aborted.
If you are going to working with a disk it is best to first get the driver's
current count value and store it here. You will then be sure that the user will
not change disks without you knowing about it.
iotd_SecLabel:
As described above, each sector has a 16 bytes long "label" are. This label
area is not used by the track disk device. However, if you want to read and/or
write to this label area you should use the "extended" read and write commands
described below, and this field should contain a pointer to some memory where
the label data can be stored/read from.
NOTE! If you intend to read some sectors' label areas you must make sure you
have allocated a buffer that is large enough. 16 bytes is needed for every
sector you intend to read. If you want to write new sector labels the memory
must of course also be large enough, but also initialized with the values you
want to write.
Since this is an extended request block you have to use the CreateExtIO()
function rather than CreateStdIO().
Synopsis
:
ext_req = CreateExtIO( msg_port, size );
ext_req:
(struct IORequest *) Pointer to the new extended request block, or NULL if the
request block could not be created.
msg_port:
(struct MsgPort *) Pointer to the message port the device should use to
communicate with you.
size:
(long) The number of bytes that should be allocated for the extended request
block. Use the function sizeof() to find the exact number of bytes needed.
If you are not going to use the "extended" commands which are described below,
but only the normal commands, you do not need to use this extended request
block. A normal standard request block will be enough. However, if you later
would change your program so it starts to use the extended commands you have to
remember to also change the type of request block. Usually it is best to always
use an extended request block, although it sometimes it not strictly necessary.
6.3.2 OPEN THE TRACKDISK DEVICE
Once you have opened a (reply) message port and created the request block you
can open the trackdisk device. This is done with help of the now for us famous
OpenDevice() function.
Synopsis
:
error = OpenDevice( name, unit, req, flags );
error:
(long) If OpenDevice() managed to open the device it returns 0, else an error
number is returned.
name:
(char *) Name of the device you want to open. The name of the trackdisk device
is (surprise) "trackdisk.device". This name has been defined is the header file
as "TD_NAME".
When you are opening devices you should always try to use the defined names
rather than the actual names. If Commodore would some day change the name of
the device (very unlikely though) you only have to recompile your program with
the new header files, and your program will work again.
unit:
(long) Which disk drive you want to use. All Amigas are sold with at leas one
internal drive (df0:), but many have bought at lest one more drive. In total
there may exist up to four disk drivers. Drive df0: has been given the unit
number 0, df1: the number 1, and so on...
If you are trying to access a drive which does not exist, OpenDevice() will
fail. See Example 2.
req:
(struct IORequest *) Pointer to the request block you have created and
initialized.
flags:
(long) This field is ignored by the trackdisk device.
6.3.3 CLEAN UP
As always, do NOT forget to clean up after yourself! All opened message ports,
request blocks, devices etc. must be closed and removed before your program may
terminate. Here is what you have to do:
1. Make sure that all your requests have been completed or aborted.
2. Close the trackdisk device with help of the CloseDevice() function.
Synopsis
:
CloseDevice( req );
reg:
(struct IORequest *) Pointer to the device's request block.
3. Delete all request blocks you have created. Note that if you have allocated
extended request blocks you must use the DeleteExtIO() function, but if you
have allocated standard request blocks you can use the DeleteStdIO() function.
As recommended above you should only use the extended request blocks with the
trackdisk device.
Synopsis
:
DeleteExtIO( std_req, size );
std_req:
(struct IOStdReq *) Pointer to the extended request block you want to delete.
size:
(long) The size of the request block, in bytes.
4. Close all message ports you have opened. Do this by calling the DeletePort()
function. Note that all messages still left at the port must be removed before
you may close the message port.
Synopsis
:
DeletePort( msg_port );
msg_port:
(struct MsgPort *) Pointer to the MsgPort structure that should be deallocated.
6.4 COMMANDS
When the trackdisk device has successfully been opened you may start to send
requests to it. The trackdisk device can do a lot of useful stuff, so there
exist several commands that could be used. These commands can be divided into
two groups. The first group consists of the "normal" commands. These commands
will not check if the disk has been removed or changed. Note also that these
commands can not be used to read or write the label areas of the sectors.
The second group consists of the "extended" commands - those commands that
needs the extended IOExtTD structure. These commands will check the unit
counter too see if the disk has been changed, and if so, the commands will
automatically be aborted. These commands can be used if you want to access the
sectors' label areas.
When you want to read and write data you have to give the "io_Data" field of
the request block a pointer to some memory where all data you read will be
placed, or if you are writing, where the data that should be written is stored.
This buffer should be "word aligned" which means that it must start on an even
byte address. The memory must also be of the type "Chip Memory" since the
blitter will be used to speed up the operations. To fulfill both demands you
should allocate the memory with help of the AllocMem function. See examples
further down for more information.
The "io_Offset" value contains the offset value from which the trackdisk device
will start to read/write from. The correct offset value is calculated with this
formula:
TD_SECTOR*( NUMSECS * 2 * cylinder + NUMSECS * head + sector )
The words in capital letters are defined in the header file
"devices/trackdisk.h" as:
TD_SECTOR 512 (512 bytes / sector)
NUMSECS 11 (11 sectors / cylinder)
For example, if you want to start to read at side 0, cylinder 5, and sector 3,
you should set the offset value to:
TD_SECTOR * ( NUMSECS * 2 * 5 + NUMSECS * 0 + 3 );
You tell the trackdisk device to read/write a specific number of bytes by
setting the "io_Length" field to the number of bytes that should be
transferred. Note that you must read complete sectors, and the length should
therefore be set to:
TD_SECTOR * "nr of desired sectors". (The trackdisk device will only read
complete cylinders, but has nothing to do with this restriction of only reading
complete sectors.)
The "io_Actual" filed will contain the number of actual bytes written/read. If
this value is not the same as the "io_Length" something has happened. Check the
"io_Error" field to find any error values. See below for more inforamtion about
error messages.
6.4.1 READ
Probably one of the most commonly used commands is undoubtedly the read
command. If you simply want to read some data, and you do not care if the user
has changed disks, you should use the "CMD_READ" command. However, most times
it is best to check if the disk has been changed, before you start to read, and
then you should use the extended "ETD_READ" command.
Here is an example: ("exreq" is a pointer to the request block, "buffer" is a
pointer to some memory which is word aligned and is of the type chip memory.
The buffer must at least be 2 * 512 bytes.)
/* We want to read: */
exreq->iotd_Req.io_Command = ETD_READ;
/* Pointer to our buffer: */
exreq->iotd_Req.io_Data = (APTR) buffer;
/* Read two sectors: */
exreq->iotd_Req.io_Length = TD_SECTORS * 2;
/* Start to read at side 0, cylinder 5, and sector 3: */
exreq->iotd_Req.io_Offset = (LONG)
TD_SECTOR*( NUMSECS * 2 * 5 + NUMSECS * 0 + 3 );
/* Do our request: */
DoIO( exreq );
6.4.2 WRITE
Same as with reading, there exist two commands to write data. The shorter
"CMD_WRITE" will simply write to the disk which is currently in the drive. The
extended "ETD_WRITE" will check that the disk has not been changed.
Here is a similar example as the one above. This time we will try to write two
sectors of data:
/* We want to write: */
exreq->iotd_Req.io_Command = ETD_WRITE;
/* Pointer to our buffer: */
exreq->iotd_Req.io_Data = (APTR) buffer;
/* Write two sectors: */
exreq->iotd_Req.io_Length = TD_SECTORS * 2;
/* Start to write at side 0, cylinder 5, and sector 3: */
exreq->iotd_Req.io_Offset = (LONG)
TD_SECTOR*( NUMSECS * 2 * 5 + NUMSECS * 0 + 3 );
/* Do our request: */
DoIO( exreq );
6.4.3 MOTOR ON/OFF
You can tell the trackdisk device to start or stop the motor of the disk drive.
Although you do not need to start the motor (this will be done automatically if
you start to read or write), you must make sure to stop the drive after you
have used it. You can either use the command "TD_MOTOR" or "ETD_MOTOR". The
later will check that the disk has not been changed.
If the "io_Length" field is set to 0 the drive will be turned off. If the field
is set to 1 the drive will be turned on. Here is an example ("exreq" is a
pointer to the request block):
/* Turn motor on/off: */
exreq->iotd_Req.io_Command = ETD_MOTOR;
/* Turn the motor on: (0 = off, 1 = on) */
exreq->iotd_Req.io_Length = 1;
/* Do our request: */
DoIO( exreq );
Remember to always turn off the motor after you have used the drive! (If the
motor already was on before you started to use it you do not need to turn it
off. This means that some other device is also using the drive.)
6.4.4 UPDATE THE DISK
When you write data to the disk it will not immediately be stored. The
trackdisk device will as explained above only read and write complete
cylinders, and if you do not write exactly an even number of cylinders, some
data will be temporarily stored in the trackdisk device's internal memory
buffer. When you later change cylinder the buffer will be copied out before the
head is moved.
You can tell the trackdisk device to immediately copy the data in the buffer to
the disk by sending a "CMD_UPDATE" or "ETD_UPDATE" command. The later command
will as usual check if the disk has been removed or changed before the buffer
is copied onto the disk.
After you have written something to the disk and before you turn the motor off
you should use this command. You will then be sure that all data you have
written will actually be on the disk.
Here is an example:
/* Update the disk (move any data still left in the buffer */
/* to the disk: */
exreq->iotd_Req.io_Command = ETD_UPDATE;
/* Do our request: */
DoIO( exreq );
6.4.5 CLEAR BUFFER
You can tell the trackdisk device to clear it's internal buffer by sending a
"CMD_CLEAR" or "ETD_CLEAR" command. Be sure that you do not clear anything that
still has not been moved out onto the disk.
This clear command is recommended to use after you have noticed that the disks
have been changed. This will ensure that old data from another disk will not be
written to the new disk.
Here is an example:
/* Clear the temporary buffer: */
exreq->iotd_Req.io_Command = ETD_CLEAR;
/* Do our request: */
DoIO( exreq );
6.4.6 POSITION THE HEAD
You can move the head to a specific position by sending a "TD_SEEK" or
"ETD_SEEK" command. The head will simply be moved to the specified position in
the "io_Offset" field, but no data will be read or written. This command is
usually not needed since the head will always be placed on the correct position
when you read or write data.
Here is an example:
/* Position the head: */
exreq->iotd_Req.io_Command = ETD_SEEK;
/* Move to position: side 1, cylinder 2, and sector 4: */
exreq->iotd_Req.io_Offset = (LONG)
TD_SECTOR*( NUMSECS * 2 * 2 + NUMSECS * 1 + 4 );
/* Do our request: */
DoIO( exreq );
6.4.7 FORMAT
With help of the "TD_FORMAT" or "ETD_FORMAT" command you can format a cylinder.
You give the "io_Data" field a pointer to some data which will be written onto
the new cylinder, and set the "io_Length" to NUMSECS * TD_SECTOR (one
cylinder). The "io_Offset" field should be set to an offset value to the
cylinder you want to format.
If you write data to a cylinder and you receive a hard write error (the
cylinder is not formatted), you can try to reformat that cylinder. Note that it
is usually best to tell the user to use some other disk rather than trying to
correct this one. The user can then try to use "Diskdoctor" or similar programs
to solve the disk problem.
Here is an example:
/* We want to format one cylinder: */
exreq->iotd_Req.io_Command = ETD_FORMAT;
/* Pointer to our buffer: (Must be at least one */
/* cylinder of bytes large, NUMSECS * TD_SECTOR. */
exreq->iotd_Req.io_Data = (APTR) buffer;
/* Format one cylinder: */
exreq->iotd_Req.io_Length = NUMSECS * TD_SECTOR;
/* Format cylinder 5 on side 0: */
exreq->iotd_Req.io_Offset = (LONG)
TD_SECTOR*( NUMSECS * 2 * 5 + NUMSECS * 0 );
/* Do our request: */
DoIO( exreq );
6.4.8 REMOVE
You can send a "TD_REMOVE" command to the trackdisk device, and it will then
increase the driver's count value. It will look like that the user has changed
disks.
Here is an example:
/* Change disks: */
exreq->iotd_Req.io_Command = TD_REMOVE;
/* Do our request: */
DoIO( exreq );
6.4.9 GET THE DISK'S CURRENT COUNT NUMBER
The "extended" commands check if the disk has changed or not before they are
executed. If the disk has changed the command is aborted. The commands compare
the "iotd_Count" value of the request block with the disk's current count
value. If the disk's count value is larger than the "iotd_Count" the command is
aborted and the error flag "TDERR_DiskChanged" is set.
Before you use these extended commands you should therefore get the disk's
current count value and store it in the request block. To get the disk's
current count value you send an extended request block with the "TD_CHANGENUM"
command set to the trackdisk device. The device will as soon as possible return
your request with a copy of the current count value in the "io_Actual" field of
the request block.
Here is an example:
/* Store the current count value here: */
ULONG count_value;
/* Get the disk's change count value: */
extreq->iotd_Req.io_Command = TD_CHANGENUM;
/* Do our request, and return when completed: */
DoIO( extreq );
/* Save the count value in the variable: */
count_value = extreq->iotd_Req.io_Actual;
After you have got the current count value you should give it the the request
blocks which should later be used. The request block can then use the extended
commands. Example:
/* Give the request block the current count value: */
extreg->iotd_Count = count_value;
After your program has received a "TDERR_DiskChanged" you should try to get the
new count value before you attempt to access the new disk.
6.4.10 CHECK IF THERE IS A DISK IN THE DRIVE OR NOT
With help of the trackdisk device can you check if there is a disk in a drive
or not. To do this you use the "TD_CHANGESTATE" command. If there is a disk in
the drive will the "io_Actual" field of the request will contain zero. On the
other hand, if there is not a disk in the drive the field will contain a
non-zero value.
/* Check if there is a disk in the drive or not: */
extreq->iotd_Req.io_Command = TD_CHANGESTATE;
/* Do our request, and return when completed: */
DoIO( extreq );
/* Is there a disk in the drive? */
if( extreq->iotd_Req.io_Actual )
printf( "No disk in the drive!\n" );
else
printf( "There is a disk in the drive!\n" );
6.4.11 CHECK IF THE DISK IS WRITE PROTECTED OR NOT
It can sometimes be very useful to know if the disk is write protected or not.
With help of the "TD_PROTSTATUS" command we can easily check this. If the disk
is write protected the "io_Actual" field will contain a non zero value. On the
other hand, if the disk is not write protected the field will contain zero.
/* Check if the disk is write protected or not: */
extreq->iotd_Req.io_Command = TD_PROTSTATUS;
/* Do our request, and return when completed: */
DoIO( extreq );
/* Is the disk write protected or not? */
if( extreq->iotd_Req.io_Actual )
printf( "The disk is write protected!\n" );
else
printf( "The disk is not write protected!\n" );
6.4.12 GET DRIVE TYPE
The trackdisk device can also be used to tell you what type of disk drives are
connected. If you send a request block with the "TD_GETDRIVETYPE" command set,
the "io_Actual" field of the request block will either contain the flag
"DRIVE3_5" or "DRIVE5_25" when it is returned. The "DRIVE3_5" means that it is
a normal 3 1/2" disk drive, and the "DRIVE5_25" means that it is a 5 1/4 (IBM)
disk drive.
/* Check drive type: */
extreq->iotd_Req.io_Command = TD_GETDRIVETYPE;
/* Do our request, and return when completed: */
DoIO( extreq );
/* What type is it? */
if( extreq->iotd_Req.io_Actual == DISK3_5 )
printf( "It is a normal 3 1/2\" disk drive.\n" );
else
if( extreq->iotd_Req.io_Actual == DISK5_25 )
printf( "It is a 5 1/4\" disk drive.\n" );
else
printf( "It is a very strange disk drive!\n" );
6.4.13 GET THE NUMBER OF TRACKS
The "TD_GETNUMTRACKS" command is used to check how many tracks are used by the
drive. When the request is returned you can check the "io_Actual" field of the
request to get the number of tracks. A normal 3 1/2 disk drive uses 160 tracks
(80 tracks / side).
/* Get the number of tracks this drive is using: */
extreq->iotd_Req.io_Command = TD_GETNUMTRACKS;
/* Do our request, and return when completed: */
DoIO( extreq );
/* How many tracks? */
printf( "No tracks: %d\n", extreq->iotd_Req.io_Actual );
6.5 ERRORS
When you are using the trackdisk device you will most definitely receive many
errors. The "io_Error" field of the requestblock will contain 0 if the request
was successfully executed, but if something failed it will contain one of the
following error numbers: (Defined in the header file "devices/trackdisk.h".)
Error code
Description
TDERR_NotSpecified
A strange error has occurred.
TDERR_NoSecHdr
Could not find a sector.
TDERR_BadSecPreamble
Strange sector.
TDERR_BadSecID
Also a strange sector.
TDERR_BadHdrSum
Strange header.
TDERR_BadSecSum
Strange data areas.
TDERR_TooFewSecs
Missing some sectors.
TDERR_BadSecHdr
Yet another strange sector.
TDERR_WriteProt
The disk is write protected.
TDERR_DiskChanged
The disk has been removed/changed.
TDERR_SeekError
Could not reach track zero.
TDERR_NoMem
Not enough memory.
TDERR_BadUnitNum
Wrong unit number.
TDERR_BadDriveType
Strange drive type.
TDERR_DriveInUse
Someone is already using the drive.
TDERR_PostReset
Tea time, the system is resetted.
There exist also four general device errors which all devices may use: (Defined
in header file "exec/errors.h".)
IOERR_OPENFAIL
Could not open the device.
IOERR_ABORTED
Request aborted.
IOERR_NOCMD
Not a valid command.
IOERR_BADLENGTH
Bad length or value.
6.6 EXAMPLES
Example 1
This program will use the Trackdisk Device to turn on and off the internal disk
drive's motor.
Example 2
This program demonstrates how you can check what went wrong while you were
using the Trackdisk Device. This example will try to use drive DF3:, which most
of us does not have, and thus we will receive an error message. (Well if you
have four disk drives connected to your Amiga there will not be any error
message.)
Example 3
This example demonstrates how you can read data with help of the Trackdisk
Device. You give this program four arguments (drive, head, cylinder and
sector), and it will print out all data in that sector. You only have to expand
this program a little and you will end up with a nice disk viewer.
Example3 drive (0-3) head (0-1) cylinder (0-79) sector (0-10)
Example 4
This example contains a lot of small and useful functions that does almost
everything you ever would like to do with the trackdisk device. The example has
been written so you can easily use the functions in your own programs.
SERIAL DEVICE
7.1 INTRODUCTION
All Amiga models have a serial port to which you can connect external devices
like a modem, scanner or a printer with a serial interface. Data can be sent in
both directions, in several different formats, like seven or eight bits, with
or without error checking etc.
On the Amiga the serial port consists of a 25 pin connector, and can
communicate with baud rate up to 32250. It is the custom chip called "Paula"
which contains a Universal Asynchronous Receiver/Transmitter (UART) that takes
care of all serial data transmissions. The chip itself can manage up to one
million bits per second, but at that speed the data buffer would be filled
before the system had time to react, and normal cables can not manage to
transmit more than 100,000 - 200,000 bits per second.
Baud rates between 1200 and 9600 are in most cases more than sufficient, and at
that speed the computer will still be able to multitask properly.
7.2 THE SERIAL PORT
The serial port sends and receives data in streams of bits. In theory only one
physical wire would therefore be required, but there have been several extra
cables added to send and receive special information as well as to supply
external devices with power (+12V) and so on.
Since all data is sent bit by bit, and not as with the parallel port which
sends one byte (8 bits) each time, it is rather slow. (Well at least not as
fast as the parallel port, which is fully documented in the next chapter.)
However, since some applications only works with one line, for example the
modems which are connected to the telephone system, serial communication is
easier to handle. Serial cables are also much cheaper.
Although only one line is required there is a whole 25-pin connector on the
Amiga. Normal applications like a modem, or a scanner only needs some of the
lines, but since there are so many possible lines to use, a lot of special
equipment can be connected to the serial port. (See illustration "RS-232".)
7.2.1 BYTE TO BITS AND VICE VERSA
Since the serial device only sends a stream of bits, a special chip has to
convert the data (bytes) to bits (8). The character "A" would for example be
transformed to 01000001 and "B" to 01000010 and so on. Of course the chip must
also be able to do the opposite, to convert an incoming stream of bits to
bytes.
--------
A (64) <- | | <- 01000001 (Receiving data)
| UART |
D (68) -> | | -> 01000100 (Transmitting data)
--------
The special chip that takes care of this is the "Universal Asynchronous
Receiver/Transmitter" usually referred to as "UART". (See illustration "UART")
Luckily we do not need to bother too much about this. However, it is good to
know what is actually happening.
7.2.2 PIN ASSIGNMENT
The Amiga's serial port is a 25-pin D-female-type connector. Below is an almost
complete list of the pin assignment, together with a short description. (See
illustration "RS-232".)
Pin Amiga RS232 HAYES Direction Type Description
-------------------------------------------------------------------
1 SHIELD GND GND - Standard Ground
2 TXD TXD TXD Out Standard Transmit data
3 RXD RXD RXD In Standard Receive data
4 RTS RTS x - Out Standard Request to send
5 CTS CTS x CTS In Standard Clear to send
6 DSR DSR x DSR In Standard Data set ready
7 GND GND GND - Standard Signal ground
8 DCD DCD DCD In Standard Carrier detect
9 +12V - - - Amiga +12 Volt
10 -12V - - - Amiga -12 Volt
11 AUDO - - Out Amiga Audio output
12 - S.DS SI + - Speed indicate
13 S.CTS - + - RS232 code
14 - S.TXD - + - RS232 code
15 - TXC - + - RS232 code
16 - S.RXD - + - RS232 code
17 - RXC - + - RS232 code
18 AUDI - - In Amiga Audio in (not used)
19 - S.RTS - + - RS
20 DTR DTR x DTR Out Standard Data terminal ready
21 - SQD - - * (A1000 +5V)
22 RI RI RI In Standard Ring indicator
23 - SS - - * (A1000 +12V)
24 - TXC1 - - - 3.58 MHz clock
25 - - - - Amiga Reset (A1000)
(x) Used only if you have set the "seven-wire flag". Se below for more
information.
WARNING! Pin 21 and 23 are connected to the power supply on the old Amiga 1000s
(+5V and +12V). On all other models the power is sent through pin 9 and 10. Be
careful with this if you intend to make serial cables! We do not want to burn
the user's external devices, do we?
7.2.3 THE DATA STREAM
Since all data is sent and received in a stream of bits, the UART chip needs to
know when each new byte (set of 8 bits) is coming in. To make this possible,
following rules have been stated: (See illustration "Serial Bits".)
1. When no data is sent, a mark bit (1) is sent over and over.
2. Just before a byte is sent, a start bit (0) is transmitted. The receiver
will now know that seven or eight bits (depends on what type of device it is)
of data will be received.
3. An optional "parity" bit will come directly after the data bits. The parity
bit is used for error checking. The receiver may use "even", "odd" or no parity
at all. If the receiver is using even parity, the sender should set the parity
bit to 1 if the remainder of all data bits divided by two is 0, else the parity
bit should be set to 0. If the receiver is using odd parity, the sender should
set the parity bit to 0 if the remainder of all data bits divided by two is 0,
else the parity bit should be set to 1. If the receiver does not use parity
checking, either set the parity bit to 0 or do not send any parity bit at all.
4. Finally we need to send one (or two) stop bit(s) to tell the receiver that
the last data bit has been sent.
...1 0 0 0 1 0 0 0 1 0 1 0 0 0 1... -> -> ->
..._ _ _ _ _...
\_____/ \_____/ \_/ \_____/
^ ^
| ^ ^ ^ 8 7 6 5 4 3 2 1 0 ^ |
| | | | <-- Data bits --> | Mark bit(s)
| | | | (7 or 8 bits) |
| | | | Start bit
| | | |
| \ / Parity bit (We use even parity, hence 0)
| |
| One or two stop bits
|
Mark bit(s)
Even parity:
The sum of the data bits is divided by two, and if the remainder is 0, the
parity bit should be set to 1. If the remainder is 1, the parity bit should be
set to 0.
10100110 = 4 bits are set. 4/2 remainder = 0 -> parity = 1
01110110 = 5 bits are set. 5/2 remainder = 1 -> parity = 0
Odd parity:
The sum of the data bits is divided by two, and if the remainder is 0, the
parity bit should be set to 0. If the remainder is 1, the parity bit should be
set to 1.
10100110 = 4 bits are set. 4/2 remainder = 0 -> parity = 0
01110110 = 5 bits are set. 5/2 remainder = 1 -> parity = 1
The nice thing about the serial device is that we do not need to bother about
this. (So why did I write it?) You only have to tell the device if you want to
use parity, and if so if it should use even (default) or odd parity. You should
also tell the serial device if you want to send seven or eight data bits, and
one or two stop bits. Once you have told the device what you want, you do not
need to think about it any more.
7.3 THE SERIAL DEVICE
The Serial Port is a limited resource it that sense that there exist (normally)
only one port. There may however be several programs running at the same time
which all want to use the serial port. To make this possible they all have to
coordinate their work with the port. That is the serial device's main task.
You can tell the device that you want a shared access, which means that other
programs may also use the serial port, or if you want exclusive access, no
other programs may use the device. It is then up to the device if it will
accept your demands or else deny you the right to use the serial port. (Another
task may have exclusive access.)
Once you have been allowed to use the serial port, you should send all your
demands to the serial device, and it will take care of everything. You only
have to give it some information like how it should work (eight or seven bits
data, parity? one or two stop bits, baud rate etc...) and where data should be
fetched/stored.
7.3.1 PREPARE THE SERIAL DEVICE
Before you should open the serial device you need to decide if you want
exclusive or shared access, and if you want to use the special RTS/CTR, DTR/DSR
("seven-wire-mode").
1. If you want shared access, other programs may also use the serial port, set
the flag SERF_SHARED. (Se below.) If you want exclusive access you do not need
to set any flags, since it is the default mode.
2. If you want to use the RTS/CTR, DTR/DSR handshaking protocol
("seven-wire-mode") set the flag SERF_7WIRE.
7.3.2 OPEN THE SERIAL DEVICE
As with all devices you have to open a message port through which the serial
device can talk to you, and allocate a request block (a IOExtSer structure),
before you may open the device itself.
1. Open a message port: (Since it is only our task and the device that will use
the message port, we do not need to make it "public", hence no name. Priority
should as usual be set to 0, normal priority.)
struct MsgPort *replymp;
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
2. Allocate a request block of type IOExtSer structure. (The IOExtSer structure
is an extended version of the normal request block, and should therefore be
allocated with help of the CreateExtIO() function with the size set to sizeof(
struct IOExtSer ).
struct IOExtSer *serial_req;
serial_req = (struct IOExtSer *)
CreateExtIO( replymp, sizeof( struct IOExtSer ) );
if( !serial_req )
clean_up( "Not enough memory!" );
Once the message port and the request block have successfully been created, you
should set the desired flags which were explained above, and then open the
serial device.
3. Set desired flags (only SERF_SHARED and SERF_7WIRE are accepted), and open
the device.
UBYTE error;
serial_req->io_SerFlags = SERF_SHARED|SERF_7WIRE;
error = OpenDevice( SERIALNAME, 0, serial_req, 0 );
if( error )
clean_up( "Could not open the Serial Device!" );
7.3.3 SET SERIAL PARAMETERS
Once the serial device has successfully been opened you should set desired
parameters like baud rate, seven or eight data bits and so on. To set these
parameters you initialize the request block as described below, set the flag
SDCMD_SETPARMS and tells EXEC to execute your request with a DoIO() command.
Here is a list of parameters that should be initialized: (We assume you have
already opened the device as described above, and have also created an IOExtSer
structure with a pointer named "ioreq" to it.
1. The serial device collects data and stores it in a temporary buffer. Because
of this it can read a lot of data without any interruption, and once there is a
break the data in this temporary buffer is moved to the real data buffer. The
temporary buffer should be at least 512 bytes, but more is recommended,
especially if you want to collect a lot of data at high speed.
This temporary buffer is automatically created by the serial device, and also
automatically deallocated when you close the device. If you have set one buffer
size, and later on changes the size, the old buffer will be removed and a new
buffer allocated. All data will then be lost, so do not change the size of the
buffer while you are still collecting data.
The size is set in the io_RBufLen field of the request structure. The size must
be at least 512, but other recommended sizes are 1024, 2048, 4096, 8192 or
16384. To set the internal input buffer size to 8192 bytes do like this:
ioreq->io_RBufLen = 8192;
2. You must tell the serial device at what speed it should communicate (how
many bits per second). It is important that both the sender and receiver are
using the same speed.
You set the desired speed by initializing the io_Baud field to one of the
following baud rates (you may use other speeds, but most applications expect
you to use on of the recommended speeds, and if it is to fast data can be lost
or corrupted): 110, 300, 1200, 2400, 4800, 9600, 19200 or 31250. To set the
baud rate to 9600 do like this:
ioreq->io_Baud = 9600;
3. Your program or the external device you are communicating with can send a
"break message", which will pause the transmission for a specified time. This
is useful if you have received to many messages and you can not answer all of
them before more are arriving. A small break would in this case be appreciated.
A break signal simply stops the communication for a specified number of
microseconds.
You can set the number of microseconds the communication should be down after a
break signal by initializing the io_BrkTime field. To set the break time to
half a second do like this:
ioreq->io_BrkTime = 500000;
4. If you want to read normal ASCII characters you only need the first seven
bits, and should therefore only try to collect seven data bits instead of
eight. However, sometimes you want to receive whole bytes, and should then
collect all eight data bits.
To set the number of bits you want to read each time you initialize the
io_ReadLen field accordingly. To read eight data bits do like this:
ioreq->io_ReadLen = 8;
5. Since you sometimes only want to send 7 data bits, but other times want send
all 8 data bits, you should initialize the io_WriteLen field accordingly. To
write eight bits do like this:
ioreq->io_WriteLen = 8;
6. After each character you need to send one or two stop bit(s), depending on
what the other device expect. Normally you only use one stop bit, but if you
are using seven data bits it happens that you should use two stop bits. To tell
the serial device to use one stop bit to like this:
ioreq->io_StopBits = 1;
7. There exist some special modes that are selected by setting some serial
flags. Here is a complete list of possible flags together with a short
description:
SERF_XDISABLED
Set this flag if you want to turn off the XON-XOFF feature. Default is on.
SERF_EOFMODE
Set this flag if you want the Serial Device to send/collect data until it finds
one of the eight specified "end-of-file" characters. (See below for more
information.)
SERF_SHARED
Set this flag if you want to share the serial port with other programs. Note
that this flag should only be used BEFORE you have opened the device, and
should NOT be changed later on. If you want to change status you should close
the serial device, alter the status and then try to open it again.
SERF_RAD_BOOGIE
Set this flag if you want to use the serial port at high speed. When this flag
is set no parity is used, xON/xOFF handling is turned off, no break signals are
allowed, and finally only eight-bit characters are used. When you need fast
communication set this flag, but remember that the data will then not be error
checked, and break signals are ignored.
SERF_QUEUEDBRK
Set this flag if you want break commands to be queued along with all other
signals. The default is that a break command interrupts the process
immediately.
SERF_7WIRE
If the flag is set, the seven-wire "handshaking" mode will be used. (Default is
three-wire.) With this flag set you can use the RS232 RTS/CTS, DTR/DSR
commands. This flag should only be used BEFORE when you have opened the serial
device, and should NOT be changed after that. To change this mode, you must
close the device, set the desired mode and then try to open it again.
SERF_PARTY_ODD
Set this flag if you want to use odd parity. (The default setting is even
parity.)
SERF_PARTY_ON
Parity checking/writing is turned on. (The sum of all data bits are divided by
two, and the remainder is the parity bit. If even parity is used the bit will
be set to 1 if the remainder is even. If odd parity is used the parity bit will
be set to 0 if the remainder is even.
You set desired flags in the io_SerFlags filed of the request structure. If you
want to set several flags, put a "|" ("binary-OR") between. To set the parity
on, and use the end of character mode, do like this:
ioreq->io_SerFlags = SERF_PARTY_ON | SERF_EOFMODE;
Note! If you do not want to set any flags, remember to set the field to 0. If
you do not make sure it is empty, some flags might accidentally be set.
8. There exist a special field where more serial flags can be used in the
future. For the moment there exist only two flags for this field.
SEXTF_MSPON
Set this flag if you want to use mark-space parity rather than odd-even parity.
SEXTF_MARK
If this and the SEXTF_MSPON flag is set, it will Mark.
If you want to use the mark-space method, with mark, do like this:
ioreq->io_ExtFlags = SEXTF_MSPON | SEXTF_MARK;
Note! If you do not want to set any of these flags, remember to set the field
to 0!
9. If you have set the flag "SERF_EOFMODE", the serial device will send/collect
data until it finds one of eight specified "end-of-file" characters. These
eight characters are stored in a IOTArray structure which look like this:
struct IOTArray
{
ULONG TermArray0;
ULONG TermArray1;
};
Each ULONG data consists of four bytes, and in total you can store eight bytes
(end-of-file characters) in the array. To make the checking routine efficient
you must store the characters in descending order! To copy the desired
characters into the array do like this:
/* Here is an array with all EOF characters: */
/* NOTE! They MUST be in descending order! */
UBYTE eof_char[8]={ 0x54, 0x32, 0x16, 0x15,
0x12, 0x03, 0x00, 0x00 };
/* Declare a unsigned byte pointer: */
UBYTE *ptr;
/* Simple loop variable: */
int loop;
...
/* Get the address of the IOTArray: */
ptr = (UBYTE *) &(ioreq->io_TermArray);
/* Set all eight end of file characters: */
for( loop=0; loop < 8; loop++ )
{
/* Copy character after character: */
*ptr = eof_chars[ loop ];
/* Step one byte foreward: */
ptr++;
}
Once you have set all desired parameters in the request block (IOExtSer
structure) you set the IO command to SDCMD_SETPARAMSand tell the serial device
to do your request by calling the DoIO() function. If something went wrong
DoIO() will return with an error number, else 0 is returned which means that
your request have successfully been executed. (See below for a complete list of
error messages.)
ioreq->IOSer.io_Command = SDCMD_SETPARAMS;
error = DoIO( ioreq );
7.3.4 READ DATA
When you want to read data from the serial port, you have to do four things.
First you have to give the serial device a pointer to a buffer where all data
which you read will be stored. (This memory buffer is not the same as the
serial device's own temporary input buffer that was explained above.)
Secondly you have to tell the device how many bytes/characters you want to
read. You do it by either telling the device exactly how many characters you
want to read, or set the length to -1 which means you want to read continuously
and stop first when an end-of-file characters have been found. (Remember to set
the flag "SERF_EOFMODE", or the serial device will not check for termination
characters.)
Thirdly you have to set the command flag "CMD_READ", which tells the serial
device that you want to read data from the serial port.
Finally you tell the serial device to do your request. You can either put your
program to sleep while the serial device is reading data with a DoIO() call, or
you use the SendIO() function and continue to do something while all data is
collected. There exist even a special "quick" mode which should be used if very
fast communication is needed.
Here is an example on how to read data: (Your program will be put to sleep
while the data is collected.)
error:
(UBYTE) is a simple unsigned byte variable.
ioreq:
(struct IOExtSer *) is a pointer to an IOExtSer structure.
data:
(BYTE) is a pointer to a block of memory where all data which is collected will
be stored. Note that the serial device will not check if it sends more data
than actually can be stored in the buffer. Innocent memory can be corrupted if
you do not make the buffer big enough!
/* We want to read some data: */
ioreq->IOSer.io_Command = CMD_READ;
/* Give the start address of our data buffer: */
ioreq->IOSer.io_Data = data;
/* We want to read 400 bytes/characters: (The buffer must */
/* then be at least 400 bytes.) */
ioreq->IOSer.io_Length = 400;
/* Do our request: */
error = DoIO( ioreq );
/* Everything OK? */
if( error )
printf( "Problems while reading!\n" );
The program above will go to sleep while the data is collected from the serial
port. However, sometimes you may want to do something while the data is fetched
and not go to sleep. Instead of using the function DoIO() you should then use
the asynchronous SendIO(). As explained in chapter 17 DEVICES, the SendIO()
will not wait for the request to be completed.
To check if the request have been completed or not you can either use the
function CheckIO(), or wait for a message to arrive at the request block's
reply port.
Once the request have been completed you can look at the io_Error field of the
request block to check if everything was OK. If the field is zero everything
was executed without any problems, but if it is non zero the request failed.
Here is an example:
/* Declare a pointer and set it to NULL: */
struct IOExtSer *ptr = NULL;
...
/* We want to read some data: */
ioreq->IOSer.io_Command = CMD_READ;
/* Give the start address of our data buffer: */
ioreq->IOSer.io_Data = data;
/* We want to read 400 bytes/characters: (The buffer must */
/* then be at least 400 bytes.) */
ioreq->IOSer.io_Length = 400;
/* Do our request and return immediately: */
SendIO( ioreq );
/* As long as the pointer is not pointing to */
/* the request we should stay in the loop: */
while( ptr == NULL )
{
... do something ...
/* Check if the request has been completed: (If the */
/* request has been completed CheckIO() will return */
/* a pointer to the request, else NULL is returned.) */
ptr = CheckIO( ioreq );
}
/* Remove the request block's message. (The ptr and ioreq */
/* are in this example identical, so it does not matter */
/* whichever you will use. The parenthesis around the */
/* expression is actually unnecessary, but this looks */
/* better.) */
Remove( &(ptr->IOSer.io_Message.mn_Node) );
/* Everything OK? Check the io_Error filed: */
if( ioreq->IOSer.io_Error )
printf( "Problems while reading!\n" );
Finally their exist a special "quick" mode. It can only sometimes be used and
only for reading. This special modecan be used when a lot of data at a very (!)
high speed should be collected. The "quick" mode is not the same as the
"SERF_RAD_BOOGIE" mode explained above. The quick mode can only be used for
reading, and there are some special restrictions, while the high speed rad
boogie mode can be used both for reading and writing.
To use the quick mode you set the IOF_QUICK flag, before you send the request.
You should then not use the normal DoIO() or SendIO() functions, but instead
the low level BeginIO() function. The BiginIO() is synchronous (like DoIO()) if
we were allowed to use quick mode, else the request is asynchronous (like
SendIO()).
Although you want to use the quick mode, it is not sure you are allowed to do
it. For some requests it is OK, others will automatically remove the IOF_QUICK
flag. You must therefore check the request after you have posted it to see if
the IOF_QUICK flag is still on or not. If the flag was removed, the quick mode
was turned off and the request should now be treated as those started with a
SendIO() call (remember to remove the message). However, it the flag is still
there the quick mode was used and we do not need (nor should) remove any
message at the reply port.
/* Initialize the request block as normal. */
/* ... */
/* Try to use the "quick" mode: */
ioreq->IOSer.io_Flags = IOF_QUICK;
/* Do the request: */
BeginIO( ioreq );
if( (ioreq->IOSer.io_Flags & IOFQUICK) )
{
/* OK! The request was using quick mode, which means */
/* that our task was put to sleep while the data was */
/* fetched, and the request have now been completed. */
/* Since we are using quick mode there are no message */
/* that should be removed. */
}
else
{
/* Too bad, we were not allowed to use the quick mode. */
/* The request should now be treated as if it was */
/* started with a SendIO() function call. This request */
/* is asynchronous - request returns immediately. */
/* We can either use the routine above with CheckIO() */
/* or put our task to sleep while we are waiting, by */
/* calling the WaitIO() function. The WaitIO() */
/* function will automatically remove the message, so */
/* we do not need to do anything more. However, if we */
/* used the function Wait(), we have to remove the */
/* messages ourself. */
WaitIO( ioreq );
}
The "quick" mode can only sometimes be used:
1. You may only read data. The "quick" mode is not allowed for writing.
2. There is enough data in the internal input buffer so the request will
immediately be satisfied.
3. No other requests is using/waiting for the serial port.
7.3.5 WRITE DATA
When you want to write data to the serial port it is almost the same procedure
as when reading. The only difference is that you set the command flag
"CMD_WRITE".
Here is an example on how to write data to the serial port: (Your program will
be put to sleep while the data is moved to the serial port.)
error:
(UBYTE) is a simple unsigned byte variable.
ioreq:
(struct IOExtSer *) is a pointer to an IOExtSer structure.
data:
(BYTE) is a pointer to a block of memory where all data you want to send is
stored.
/* We want to send (write) some data: */
ioreq->IOSer.io_Command = CMD_WRITE;
/* Give the start address of our data buffer: */
ioreq->IOSer.io_Data = data;
/* We want to send 150 bytes/characters: */
ioreq->IOSer.io_Length = 150;
/* Do our request: */
error = DoIO( ioreq );
/* Everything OK? */
if( error )
printf( "Problems while writing!\n" );
The program above will go to sleep while the data is copied to the serial port.
If you do not want the program to wait for the request to be completed you
should use SendIO(). The procedure is very similar with reading.
To check if the request have been completed or not you can either use the
function CheckIO(), or wait for a message to arrive at the request block's
reply port.
Once the request have been completed you can look at the io_Error field of the
request block to check if everything was OK. If the field is zero everything
was executed without any problems, but if it is non zero the request failed.
Here is an example:
/* Declare a pointer and set it to NULL: */
struct IOExtSer *ptr = NULL;
...
/* We want to send (write) some data: */
ioreq->IOSer.io_Command = CMD_WRITE;
/* Give the start address of our data buffer: */
ioreq->IOSer.io_Data = data;
/* We want to send 280 bytes/characters: */
ioreq->IOSer.io_Length = 280;
/* Do our request and return immediately: */
SendIO( ioreq );
/* As long as the pointer is not pointing to */
/* the request we should stay in the loop: */
while( ptr == NULL )
{
... do something ...
/* Check if the request has been completed: (If the */
/* request has been completed CheckIO() will return */
/* a pointer to the request, else NULL is returned.) */
ptr = CheckIO( ioreq );
}
/* Remove the requst block's message. (The ptr and ioreq */
/* are in this example identical, so it does not matter */
/* whichever you will use. The parenthesis around the */
/* expression is actually unnecessary, but this looks */
/* better.) */
Remove( &(ptr->IOSer.io_Message.mn_Node) );
/* Everything OK? Check the io_Error filed: */
if( ioreq->IOSer.io_Error )
printf( "Problems while writing!\n" );
You can not use the quick mode while you are sending data. If you need fast
communication you should set the serial flag "SERB_RAD_BOOGIE". Note the
restrictions mentioned above. (Only eight-bit characters, and no parity
checking etc...)
Since you usually will want to read and write data at the same time, you have
to use two separate request blocks. If you are using shared access mode you
simply create two request blocks and open the serial device twice. (Do not
forget to close both requests later on.) Here is an example:
struct MsgPort *replymp;
struct IOExtSer *serial_req_read;
struct IOExtSer *serial_req_write;
UBYTE error;
/* We use only one reply message port: */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* Create the request block "read": */
serial_req_read = (struct IOExtSer *)
CreateExtIO( replymp, sizeof( struct IOExtSer ) );
if( !serial_req_read )
clean_up( "Not enough memory!" );
serial_req_read->io_SerFlags = SERF_SHARED|SERF_7WIRE;
/* Create the request block "write": */
serial_req_write = (struct IOExtSer *)
CreateExtIO( replymp, sizeof( struct IOExtSer ) );
if( !serial_req_write )
clean_up( "Not enough memory!" );
serial_req_write->io_SerFlags = SERF_SHARED|SERF_7WIRE;
/* Open the serial device for the read request: */
error = OpenDevice( SERIALNAME, 0, serial_req_read, 0 );
if( error )
clean_up( "Could not open the serial device (Read)!" );
/* Open the serial device for the write request: */
error = OpenDevice( SERIALNAME, 0, serial_req_write, 0 );
if( error )
clean_up( "Could not open the serial device (Write)!" );
If you are using exclusive access mode you can of course not open the serial
device twice. Instead we have to copy the first request block to the other
request block, byte for byte. Here is an example:
struct MsgPort *replymp;
struct IOExtSer *serial_req_read;
struct IOExtSer *serial_req_write;
BYTE *r_ptr;
BYTE *w_ptr;
UBYTE error;
int loop;
/* We use only one reply message port: */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* Create the request block "read": */
serial_req_read = (struct IOExtSer *)
CreateExtIO( replymp, sizeof( struct IOExtSer ) );
if( !serial_req_read )
clean_up( "Not enough memory!" );
serial_req_read->io_SerFlags = SERF_SHARED|SERF_7WIRE;
/* Create the requestblock "write": */
serial_req_write = (struct IOExtSer *)
CreateExtIO( replymp, sizeof( struct IOExtSer ) );
if( !serial_req_write )
clean_up( "Not enough memory!" );
serial_req_write->io_SerFlags = SERF_SHARED|SERF_7WIRE;
/* Open the serial device for the read request: */
error = OpenDevice( SERIALNAME, 0, serial_req_read, 0 );
if( error )
clean_up( "Could not open the Serial Device (Read)!" );
/* Since we can not open the serial device once again */
/* for the write request, we have to copy the whole */
/* read request block into the write request block. */
/* Get the start address of both request blocks: */
r_ptr = (BYTE *) serial_req_read;
w_ptr = (BYTE *) serial_req_write;
/* Copy the request block, byte by byte: */
for( loop=0; loop < sizeof( struct IOExtSer ); loop++ )
{
/* Copy one byte: */
*w_ptr = *r_ptr;
/* Step one byte forward: */
w_ptr++;
r_ptr++;
}
Now we have two request blocks, and can therefore use both read and write
requests at the same time. If you also want to use the special extra functions
described further down, you may want to create even one more request block (or
maybe even more...). The procedure is the same as described above.
7.3.6 ERRORS
While you are using the serial device several errors may occur. The most common
problem is that some other program is already using the serial port, but it can
happen that data has been corrupted while reading/writing (the parity checking
is a simple way to find these errors).
There exit for the moment nine different types of serial port errors, all
defined in the header file "devices/serial.h". You will receive the error
message from the function you just called, or you can check a request block to
see if there were any problems. (The io_Error filed of the request block either
contains 0, which means everything is OK, or an error number.)
Here is a complete list of the serial error messages:
SerErr_DevBusy:
Some other task/request is already using the serial device.
SerErr_BufErr:
The serial device could not allocate enough memory for the internal input
buffer.
SerErr_InvParam:
The request block's parameters were not properly initialized.
SerErr_LineErr:
The serial cable is faulty or the other device is not properly initialized or
not connected. Tell the user to check the cables!
SerErr_ParityErr:
The data you just sent/received contained at least one byte which was
corrupted. Note that the serial flag "SERF_PARITY_ON" must be set, else the
device will not check for errors.
SerErr_TimerErr:
There was some problem with the timer.
SerErr_BufOverflow:
The serial input buffer has been filled. You should empty it as fast as
possible so you do not lose valuable information.
SerErr_NoDSR:
No DSR.
SerErr_DetectedBreak:
A break was detected.
While you are using the serial device it may happen that you receive other
error messages than described above. It is then usually the Exec which sent
them: (defined in the header file "exec/errors.h")
IOERR_OPENFAIL
The device (unit) could not be opened. (If you are denied access to the serial
device you should receive the SerErr_DevBusy flag instead of this exec message,
but internally this flag is used.)
IOERR_ABORTED
When you abort a previously started request by calling the AbortIO() function,
the io_Error filed of that request is set to IOERR_ABORTED. If you find a
request block with this flag set, you know that it has been aborted.
IOERR_NOCMD
You tried to use a command that is not supported by the serial device.
IOERR_BADLENGTH
T
he length of the request was not valid.
7.3.7 CLEAN UP
As usual on the Amiga you must remember to close and return everything you have
opened or allocated. If you do not close the serial device after you, other
programs will then not be able to use the serial port. PLEASE be very careful
about this!
Here is a list of what you have to do:
1. All requests you have started with SendIO() or BeginIO() (asynchronous
commands) must either have been completed or aborted before you may close the
device. It is a very common error to forget this, and it can be hard to find
this bug. Usually the program will work fine (the command was completed in
time), but now and then your program will crash (the command was completed
after the device have been closed).
A simple way is to abort all commands that have not reported that they have
been completed, but this is not always good way to do it. (The last commands
may be important and should therefore not be aborted.)
If you do not want to abort the command, you should instead wait for it to be
completed. The WaitIO() function is simple to use, and will put your program to
sleep while waiting, so no computer time is wasted. If the request has already
been completed, the function will return immediately. WaitIO() will also remove
the message from the reply port. It is a very useful and simple function to
use, but do NOT try to wait for a request that has not been started! (It will
then be a very long wait, probably some million years before the user realizes
that he/she has to hit the reset keys.)
Here is an example on how to wait for a request to be completed: (If the
request already has been completed it does not matter, WaitIO() will then
simply return immediately. Note that we do not have to remove any messages from
the reply port if we use WaitIO().)
/* Store possible error numbers here: */
UBYTE error;
/* ... */
/* Wait for the request to be completed: */
error = WaitIO( ioreq );
/* Everything OK? */
if( error )
printf( "Something went wrong!" );
/* Well, successful or not, we may now */
/* close the device! */
To abort a request, simply use the AbortIO() function:
/* Try to abort a previously started request: */
/* (Do not try to abort a request that has not */
/* been started.) */
AbortIO( ioreq );
2. When all requests have been completed or aborted you may close the serial
device. (Requests that have been executed by calling the DoIO() function have
already been completed before your program wakes up, and thus you do not need
to wait for these.)
The serial device is closed as all other devices, by calling the CloseDevice()
function. Here is an example:
/* Close the Serial Device: */
if( !serial_dever )
CloseDevice( ioreq );
3. You should now return all request blocks you have allocated. If you
allocated a standard sized request block by calling the CreateStdIO() function,
you should free it by calling the DeleteStdIO() function. However, if you have
allocated an extended request block (like the serial device's request blocks)
by calling the CreateExtIO() function, you MUST call the DeleteExtIO(), and of
course remove the same amount of data as you allocated.
How to delete a standard request block: (Since it is of the standard size, you
do not need to specify any size.)
/* Deallocate a standard sized request block: */
DeleteStdIO( ioreq );
With extended request block you have to specify the size as you did when you
allocated it:
/* Deallocate an extended request block: */
/* (The size may vary depending on what */
/* device it was used for.) */
DeleteExtIO( ioreq, sizeof( struct IOExtSer ) );
Note that ALL request blocks that have been allocated, must be removed!
4. Finally you should close all message ports you have previously opened.
Simply use the DeletePort() function as this example demonstrates:
/* Remove the replyport: */
DeletePort( replymp);
It can not be said too often. Please be careful with how your program
terminates! Your program should not only run fine, but it should also allow
other programs to run after and simultaneously. Remember that your program must
also be able to quit nice and neatly even if it had to terminate too early
because of some fatale error. The cleaning up should only be done where it is
needed, and if you have not allocated the memory or opened the device before
your program quits, you should of course NOT try to free these resources! If
you do the Amiga will most certainly crash! Too many programs contain this very
annoying error.
The best way to manage this cleaning up routine is to write a separate function
which checks each thing before it frees it. The idea is that you may call this
routine at any time, and it will still manage to clear everything properly.
Here is an example: (Note how we check each thing to see if it should be
removed!)
/* Close and return everything that has been */
/* opened and allocated before we quit: */
void clean_up()
{
/* 1. Close the Serial Device: */
if( !serial_dever )
CloseDevice( serial_req );
/* 2. Deallocate the serial request block: */
if( serial_req )
DeleteExtIO( serial_req, sizeof( struct IOExtSer ) );
/* 3. Remove the replyport: */
if( replymp )
DeletePort( replymp);
/* 4. Quit: */
exit( 0 );
}
Comments:
1. The serial_dever is a variable that we have declared ourself, and is used
only to determine if the device has been opened or not. (The variable is set to
TRUE before the program starts, and is only changed to FALSE after a successful
opening of the device. See the included examples for more information.) We
should of course only close the device if the serial_dever is FALSE.
("serial_dever" stands for "serial device error".)
2. The serial_req is a pointer which is either pointing to a request block, or
NULL if no request block has been allocated. We should of course only free the
request block if there exist one.
3. The replymp is a pointer which is either pointing to a message port, or NULL
if no message port has been opened. And of course, we should only close the
message port if it has been opened.
4. Finally our program may terminate. The exit() function is placed here so we
are sure the program will quit. (If we did not have this exit(), our function
would then return the control to our program again, and then we would have
serial (sorry, serious) problems.
7.4 A COMPLETE EXAMPLE
Here is a complete example that opens the serial device, sets all necessary
parameters, sends some data, collects some data and finally cleans up and
quits. Since we send the read and write requests after each other (the first is
completed before the other is started) we only need to use one request block.
#include <exec/types.h>
#include <devices/serial.h>
/* Declare a pointer to our reply port: */
struct MsgPort *replymp = NULL;
/* Declare a pointer to our serial request block: */
struct IOExtSer *serial_req = NULL;
/* Store the serial device error here: */
UWORD serial_dever = TRUE;
/* Declare our own data buffer: (Big enough to hold 300 bytes.) */
BYTE buffer[ 300 ];
/* Declare our functions: */
void main();
void clean_up( STRPTR text );
void main()
{
int loop;
UBYTE *ptr;
UBYTE error;
/* The eight end-of-file characters: */
UBYTE eof_char[8]={ 0x06, 0x05, 0x04, 0x03,
0x02, 0x01, 0x00, 0x00 };
/* OPEN THE SERIAL DEVICE: */
/* Get a reply port: (No name, priority 0) */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* Create a serial request block: */
serial_req = (struct IOExtSer *)
CreateExtIO( replymp, sizeof( struct IOExtSer ) );
if( !serial_req )
clean_up( "Not enough memory for the serial request block!" );
/* Open the Serial Device: */
serial_dever = OpenDevice( SERIALNAME, 0, serial_req, 0 );
if( serial_dever )
clean_up( "Could not open the Serial Device!" );
/* SET THE SERIAL PARAMETERS: */
/* Set the Serial Device's own input buffer to 512 bytes: */
serial_req->io_RBufLen = 512;
/* Set baud rate to 9600 baud: */
serial_req->io_Baud = 9600;
/* Set break time to half a second: */
serial_req->io_BrkTime = 500000;
/* Read 8 bits per character: */
serial_req->io_ReadLen = 8;
/* Write 8 bits per character: */
serial_req->io_WriteLen = 8;
/* Use 1 stop bit: */
serial_req->io_StopBits = 1;
/* Use parity and end-of-file characters: */
serial_req->io_SerFlags = SERF_PARTY_ON | SERF_EOFMODE;
/* No additional flags: */
serial_req->io_ExtFlags = NULL;
/* Set all eight end of file characters: */
ptr = (UBYTE *) &(serial_req->io_TermArray);
for( loop=0; loop < 8; loop++ )
{
/* Copy character after character: */
*ptr = eof_char[ loop ];
/* Step one byte forward: */
ptr++;
}
/* All values have now been set, lets do a SDCMD_SETPARAMS request: */
serial_req->IOSer.io_Command = SDCMD_SETPARAMS;
/* Do our request: */
error = DoIO( serial_req );
if( error )
clean_up( "Could not set the serial parameters!" );
/* SEND DATA TO THE SERIAL PORT: */
/* Put the data we want to send to the serial port into */
/* our own data buffer: (Well in this example we only */
/* send two bytes.) */
buffer[ 0 ] = 0x4C;
buffer[ 1 ] = 0x31;
/* We want to send (write) some data: */
serial_req->IOSer.io_Command = CMD_WRITE;
/* Give the start address of our data: */
serial_req->IOSer.io_Data = (APTR) buffer;
/* We want to send two bytes: */
serial_req->IOSer.io_Length = 2;
/* Do our request: */
error = DoIO( serial_req );
if( error )
clean_up( "Could not send data to the serial port!" );
/* READ DATA FROM THE SERIAL DEVICE: */
/* We want to read some data: */
serial_req->IOSer.io_Command = CMD_READ;
/* Give the start address of our buffer: */
serial_req->IOSer.io_Data = (APTR) buffer;
/* We want to read 0 bytes: */
/* (If you do not have anything connected to your serial */
/* port, we better not wait for any signals, thus read */
/* only 0 bytes.) */
serial_req->IOSer.io_Length = 0;
/* Do our request: */
error = DoIO( serial_req );
if( error )
clean_up( "Could not read data from the serial port!" );
/* THE END: */
clean_up( "The End" );
}
/* Close and return everything that has been */
/* opened and allocated before we quit: */
void clean_up( STRPTR text )
{
/* Close the Parallel Device: */
if( !serial_dever )
CloseDevice( serial_req );
/* Deallocate the serial request block: */
if( serial_req )
DeleteExtIO( serial_req, sizeof( struct IOExtSer ) );
/* Remove the replyport: */
if( replymp )
DeletePort( replymp);
/* Print the message: */
printf( "\n%s\n", text );
/* Quit: */
exit( 0 );
}
7.5 OTHER USEFUL COMMANDS
You can not only read and write from/to the serial port. There exist several
other functions that can sometimes be needed. Here is a complete list:
1. Break, stop the serial device for a short time.
2. Clear, clear the input buffer.
3. Flush, removes all queued requests.
4. Query, get some information from the serial device.
5. Reset, reinitializes the serial device.
6. Start, restarts the serial communication.
7. Stop, temporary stops the serial communication.
7.5.1 BREAK
While you are sending or reading data you sometimes have to take a small pause.
Your program may for example need to empty the data buffer, or coordinate its
action with some other task. To pause the serial device you simply send a break
request, and all communications is halted for a specified time.
You send a break signal by setting the io_Command field to "SDCMD_BREAK". The
default is that all serial requests are immediately halted, but if the serial
flag "SERF_QUEUEDBRK" is set, the break command will be queued as all other
requests and the device will first take a break when all previous requests have
been completed.
The serial device will normally take a 250000 microseconds (1/4 seconds) long
break, but you may change this break time by altering the serial device's
"io_BrkTime" parameter.
Here is an example on how you can send a break command:
/* We want to take a pause: */
ioreq->IOSer.io_Command = SDCMD_BREAK;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Problems with the break!\n" );
7.5.2 CLEAR
While you are reading data from the serial device, it is actually first stored
in the internal input buffer, and then moved to your own buffer when requested.
If you want to clear the buffer before you start to read you should set the
io_Command field to "CMD_CLEAR". Here is an example:
/* We want to clear the input buffer: */
ioreq->IOSer.io_Command = CMD_CLEAR;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not clear the input buffer!\n" );
7.5.3 FLUSH
If several requests are sent to the device they are all queued on a FIFO (First
In First Out) basis. The command "CMD_FLUSH" can then be used to remove all
these queued commands. Here is an example:
/* We want to remove all queued requests: */
ioreq->IOSer.io_Command = CMD_FLUSH;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not remove the queued requests!\n" );
7.5.4 QUERY
The command "SDCMD_QUERY" can be used to get some information from the serial
device. It is useful if you want to see the serial status and/or how many bytes
there are already in the internal input buffer. The "io_Status" field of the
request structure will be set as following table shows:
Bit
Hex
Active
Description
0 0x0001 - Reserved
1 0x0002 - Reserved
2 0x0004 High Connected to par. "select" and ser. "ring"
3 0x0008 Low DSR - Data Set Ready
4 0x0010 Low CTS - Clear To Send
5 0x0020 Low DCD - Carrier Detect
6 0x0040 Low RTS - Ready To Send
7 0x0080 Low DTR - Data Terminal Ready
8 0x0100 High Read overrun
9 0x0200 High Break sent
10 0x0400 High Break received
11 0x0800 High Transmit X-OFF
12 0x1000 High Receive X-OFF
13 0x2000 - Reserved
14 0x4000 - Reserved
15 0x8000 - Reserved
The field "io_Actual" of the request structure contains the number of bytes
still left in the internal input buffer. Here is an example:
/* Check the serial device: */
ioreq->IOSer.io_Command = SDCMD_QUERY;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not get any information from the device!\n" );
else
{
/* Check the "io_Status" field: */
if( ioreq->io_Status & 0x0200 )
printf( "A break request have just been sent!\n" );
/* Check number of characters left in the input buffer: */
printf( "Characters left: %ld\n", ioreq->IOSer.io_Actual );
}
7.5.5 RESET
Send the command "CMD_RESET" to reset the serial device. All commands that are
queued to the device will be removed, commands that are currently executed will
be aborted, the internal input buffer will be cleared and reallocated to the
default size and finally all serial flags are resetted. Here is an example:
/* We want to reset the serial device: */
ioreq->IOSer.io_Command = CMD_RESET;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not reset the serial device!\n" );
7.5.6 START
After you have stopped the serial communication by sending an X-OFF message to
the other device, you may want to start the communication again. It is done by
sending an X-ON message, and the external device will then know that it may
start to send/receive data. X-ON messages are sent to the other device (as well
to our self) by issuing a "CMD_START" command. Here is an example:
/* We want to start serial communication again: */
ioreq->IOSer.io_Command = CMD_START;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not start the serial communication!\n" );
7.5.7 STOP
To temporary stop all serial communication you send a "CMD_STOP" command. If
you try to stop the communication an X-OFF message will be sent to the external
device (as well as to all other programs currently using the serial device).
When a X-OFF message has been received, communication will first start again
when an X-ON message is sent. (See CMD_START.) Here is an example:
/* We want to temporary stop all serial communication: */
ioreq->IOSer.io_Command = CMD_STOP;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not stop the serial communication!\n" );
7.6 FUNCTIONS
DoIO()
DoIO() is used to send requests to a device, and waits for it to be completed.
While the program is waiting it is put to sleep so it will not waste any
computer time. DoIO() will return first when the request have been completed or
failed, and no message is therefore sent to the reply port.
Synopsis:
error = DoIO( req );
error:
(long) DoIO() will return first when the request has been completed or
something has failed. If the request was successfully completed zero is
returned, else an error number is returned. What error number depends on which
device was used.
req:
(struct IORequest *) Pointer to the request you want to have executed.
SendIO()
SendIO() is used to send requests to a device, but will return immediately
without any delay. To check if the request have been completed use the
CheckIO() function, or look at the request's reply port for any messages. Once
the request has been completed you must remove the message at the reply port.
(CheckIO() will not do it.) To remove a message use the function Remove(). Note
that you may NOT close the serial device before all requests have been
completed or aborted!
Synopsis:
SendIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed.
CheckIO()
CheckIO() is used to check if a previously started request has been completed.
Note that this function will not remove the message at the reply port. This
must be done with the Remove() function.
Synopsis:
ptr = CheckIO( req );
ptr:
(long) CheckIO() will either return NULL if the request have not been completed
or it will return a pointer to the request block.
req:
(struct IORequest *) Pointer to the request you want to check.
WaitIO()
WaitIO() will wait for the request to be completed, and while the program is
waiting it is put to sleep so no computer time is wasted.
Synopsis:
error = WaitIO( req );
error:
(long) WaitIO() will return first when the request, that has previously been
sent, has been completed or something has failed. If the request was
successfully completed zero is returned, else an error number is returned. What
error number depends on which device was used.
req:
(struct IORequest *) Pointer to the request you want to wait for to be
completed. Note that the request must have already been sent to the device by
either a SendIO() or BeginIO() function call.
BeginIO()
BeginIO() is a low level form of the SendIO() function. The advantage with
BeginIO() is that no fields of the request block will be altered as which is
the case with SendIO(). If you are for example using the "quick mode" you
should not use SendIO() since it will alter some of the data blocks.
BeginIO() is synchronous command if you use the quick mode, but if you are not,
the command will be asynchronous. Note that you may NOT close the serial device
before all requests have been completed or aborted, so be careful with
asynchronous commands.
Synopsis:
BeginIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed.
BeginIO() will not alter any values in this request structure as which is the
case with SendIO() and DoIO(). Normally this is not any problem, and thus the
SendIO() and DoIO() functions should be used. However, with the Audio Device
for example some of these fields which are altered should not be changed, and
you should therefore use this low level function BeginIO().
AbortIO()
AbortIO() will try to abort a previously started request. This function should
be used sparsely since it does not look so good if you start a request and the
try to stop it. (Better not start it at all.) However, it is easy, and can
sometimes be very useful.
A request that is aborted will have its io_Error field set to IOERR_ABORTED
(defined in header file "exec/errors.h").
Synopsis:
AbortIO( req )
req:
(struct IORequest *) Pointer to the request you want to abort.
CloseDevice()
CloseDevice() will (surprise!) close a device. If you close the serial device
the internal input buffer will automatically be deallocated. Note that you
should NOT close the device before all started asynchronous requests have been
either completed or aborted.
Synopsis:
CloseDevice( ioreq );
ioreg:
(struct IORequest *) Pointer to the device's request block.
OpenDevice()
OpenDevice() will try to open the specified device.
Synopsis:
error = OpenDevice( name, unit, req, flags );
error:
(long) If OpenDevice() managed to open the device it returns 0, else an error
number is returned. If you try to open the serial device, and there is already
a program that is using it, the error message "SerErr_DevBusy" is returned.
name:
(char *) Name of the device you want to open. The name of the serial device is
defined as SERIALNAME in header file "devices/serial.h".
unit:
(long) Which unit you want to open. Since there exist only one serial port,
this field is ignored.
req:
(struct IORequest *) Pointer to a request block. For the serial device it must
be a pointer to an extender serial request block (struct IOExtSer).
flags:
(long) Any special mode is set here. Ignored by the serial device.
7.7 COMMANDS
Here is a complete list of commands you may send to the serial device. For full
documentation se examples above.
The special serial device commands: (Defined in header file "devices/serial.h")
SDCMD_BREAK
Sends a break signal.
SDCMD_QUERY
Check the status of the serial device.
SDCMD_SETPARAMS
Set the parameters of the serial device.
The rest of the commands you may use are normal exec commands, and are defined
in header file "exec/io.h".
CMD_RESET
Resets all parameters of the serial device.
CMD_READ
Read data from the serial port.
CMD_WRITE
Write data to the serial port.
CMD_CLEAR
Clears the internal input buffer.
CMD_STOP
Temporary stops all serial communication. (X-OFF)
CMD_START
Restarts serial communication. (X-ON)
CMD_FLUSH
Removes all queued requests.
7.8 EXAMPLES
The included examples demonstrates how you can use the Serial Device. It
demonstrates most of the features described in this chapter. Since I do not
know what you have connected to your serial port the examples will not do very
much. However, the examples are easy modify, so it should not be hard for you
to change them as desired.
Example 1
If you have a Sharp JX-100 scanner you can run this program since it will try
to turn the lamp on and then off again. Very useful! (hmmm...) The program does
not check if there is any contact with the scanner, nor if the lamp really was
turned on or not. It simply demonstrates how to send data.
Example 2
This example is rather similar to Example 1, but this time we do not wait for
the serial port to complete our request. Instead we do somethings (well not
very much) and now and then checks if the request has been completed. Using a
busy wait.
Example 3
This example is also rather similar to Example 1, but this time we try to read
and write at the same time. To be able to do several requests simultaneously we
need one request block for each command. In this example we use three separate
request blocks. Using asynchronous commands but puts the task to sleep just
before we clear and return everything.
Example 4
This example does not do anything, but it consists of several useful functions
that you can use yourself after small modifications. The functions demonstrates
all commands there exist for the serial device, so if you had problems in
understanding how a command was used you can look here.
PARALLEL DEVICE
8.1 INTRODUCTION
All Amiga models have a parallel port to which you can connect external devices
like a printer, a video digitizer or a sound sampler. The most common external
device for the parallel port is undoubtedly a printer, although some printers
are connected to the serial device.
The parallel port can send and receive eight bits simultaneously. This can be
compared with the serial port which only can send/receive a stream of bits. The
parallel port is because of this much faster and is therefore often used for
video digitizers or sound samplers.
The parallel device helps you to work with the printer at a very low level, but
is still easy to handle. The parallel device can as the serial device be locked
for exclusive access or you can allow other programs to use the device
simultaneously.
It is important to note that the parallel device should only be used when you
want to handle the port directly at a low level. This can be useful when you
want to collect data from a video digitizer, or send untranslated printer
codes. However, if you simply want to use the printer you should use the
printer device instead. The printer device will automatically take care of all
printer handling, and is using the preference settings. See next chapter (29)
"Printer Device".
8.2 PARALLEL PORT
A parallel port sends a whole byte each time as explained above, and is
therefore very fast. Data that is sent to or received from the parallel port
does not need to be translated in any way, it is immediately usable.
The Amiga's parallel (Centronics) port is a 25-pin D-female- type connector.
(On the old A1000s the parallel port have a male connector.) Below is an almost
complete list of the pin assignment, together with a short description. (See
illustration "Centronics".)
Pin Name Direction Description
-----------------------------------------------------
1 STROBE Out Used to coordinate the events
2 Data 0 In/Out Bit 0
3 Data 1 In/Out Bit 1
4 Data 2 In/Out Bit 2
5 Data 3 In/Out Bit 3
6 Data 4 In/Out Bit 4
7 Data 5 In/Out Bit 5
8 Data 6 In/Out Bit 6
9 Data 7 In/Out Bit 7
10 ACK In Data acknowledge
11 BUSY In/Out General Input/Output pin
12 POUT In/Out General Input/Output pin
13 SEL In/Out General Input/Output pin
14 +5V - +5 Volt
15 NC In/Out No connection pin
16 RESET Out The system resets
17 GND - Signal ground
18 GND - Signal ground
19 GND - Signal ground
20 GND - Signal ground
21 GND - Signal ground
22 GND - Signal ground
23 GND - Signal ground
24 GND - Signal ground
25 GND - Signal ground
8.3 PARALLEL DEVICE
The parallel device is very similar to the serial device. It can either be used
in exclusive mode, or several programs may use the port simultaneously, shared
access. When you want that the parallel device to do something you simply send
an already initialized request block (struct IOExtPar), and the device will
send a message to the reply port when the request has been done. Exactly as all
other devices.
8.3.1 THE PARALLEL REQUESTBLOCK
The request block you should use with the parallel device look like this:
(defined in header file "devices/parallel.h")
struct IOExtPar
{
struct IOStdReq IOPar;
ULONG io_PExtFlags;
UBYTE io_Status;
UBYTE io_ParFlags;
struct IOPArray io_PTermArray;
};
IOPar:
This is the standard request block. The IOStdReq structure is defined in header
file "exec/io.h", and is fully documented in chapter 17 "Devices".
io_PExtFlags:
This is currently not used, but will maybe be used in the future when more
parallel flags are needed.
io_Status:
The status of the parallel port and parallel device. There exist for the moment
four status flags:
IOPTF_RWDIR If this flag is set the device is currently writing to the
parallel device. On the other hand, if the flag is not set the device is
collecting data at the parallel port.
IOPTF_PARSEL Printer selected.
IOPTF_PAPEROUT The printer ran out of paper. Inform the user!
IOPTF_PARBUSY The parallel port is currently busy.
Use the command "PDCMD_QUERY" before you look at these fields to make sure
everything is up to date.
io_ParFlags:
This field contains all special parallel flags. There exist only three flags
for the moment, and one of these is still not usable. Here is the complete
list:
PARF_SHARED Set this flag if you want to share the parallel port with other
programs. Note that this flag should only be altered before you have opened the
parallel device, and should NOT be changed later on.
If you want to change status you should close the parallel device, alter the
status and then try to open the device again.
PARF_RAD_BOOGIE This flag is currently not used. It is supposed to be set when
you want to send/receive data at a very high speed.
PARF_EOFMODE This is actually the only flag you may alter after you have
opened the device. If the flag is set the parallel device will immediately stop
the transmission of data when it finds one of the specified end-of-file
characters.
io_PTermArray:
This field contains eight characters which will be treated as the end-of-file
characters if the PARF_EOFMODE flag is set.
8.3.2 OPEN THE PARALLEL DEVICE
As with all devices you have to open a message port through which the parallel
device can communicate with you, and allocate a request block (a IOExtPar
structure), before you may open the device itself.
1. Open a message port: (Since it is only our task and the device that will use
the message port, we do not need to make it "public", hence no name. Priority
should as usual be set to 0, normal priority.)
struct MsgPort *replymp;
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
2. Allocate a request block of type IOExtPar structure. (The IOExtPar structure
is an extended version of the normal request block, and should therefore be
allocated with help of the CreateExtIO() function with the size set to sizeof(
struct IOExtPar ).
struct IOExtPar *parallel_req;
parallel_req = (struct IOExtPar *)
CreateExtIO( replymp, sizeof( struct IOExtPar ) );
if( !parallel_req )
clean_up( "Not enough memory!" );
Once the message port and the request block have successfully been created, you
need to decide if you want exclusive or shared access. If you want shared
access, other programs may also use the parallel port, should you set the flag
"PARF_SHARED" in the io_ParFlags" field. If you want exclusive access you do
not need to set any flags, since it is the default mode.
3. Either set shared or exclusive access, and open the device.
UBYTE error;
/* We want shared access: */
parallel_req->io_ParFlags = PARF_SHARED;
/* Open the parallel device: */
error = OpenDevice( PARALLELNAME, 0, parallel_req, 0 );
if( error )
clean_up( "Could not open the Parallel Device!" );
8.3.3 SET PARALLEL PARAMETERS
Once you have successfully opened the parallel device you may set some
parameters that tells the device how it should work. Luckily, the parallel
device is much simpler than the serial device.
For the moment there exist only one parallel flag you may use, and that is
PARF_EOFMODE. It should be set if you want the device to immediately stop the
current communication if one of the eight specified end-of-file characters
appears. To set the flag do like this:
/* Look for end-of-file characters: */
parallel_req->io_ParFlags += PARF_EOFMODE;
Note that we used the sign "+=". This is very useful if you want to set one
flag (bit), but keep all others unchanged. If we simply used the sign "=" the
"PARF_SHARED" flag we previously had set would be erased. We could of course
have written it like this:
/* Set both the end-of-file and shared mode: */
parallel_req->io_ParFlags = PARF_EOFMODE | PARF_SHARED;
This is however not equally good, since it is easy to forget one flag, and then
it would be erased. So if you want to add flags use the command "+=". To erase
one flag you only have to do the opposite and subtract it ("-=").
If you want to use the end-of-file mode, and have set the "PARF_EOFMODE" flag,
you must also tell the device which characters should be treated as end-of-file
characters. The list of these characters is stored in the "IOPArra" array which
is placed at the bottom of the request structure, and looks like this:
struct IOPArray
{
ULONG PTermArray0;
ULONG PTermArray1;
};
Each ULONG data consists of four bytes, and in total you can store eight bytes
(end-of-file characters) in the array. To make the checking routine efficient
you must store the characters in descending order! To copy the desired
characters into the array do like this:
/* Here is an array with all EOF characters: */
/* NOTE! They MUST be in descending order! */
UBYTE eof_char[8]={ 0x54, 0x32, 0x16, 0x15, 0x12, 0x03, 0x00, 0x00 };
/* Declare a unsigned byte pointer: */
UBYTE *ptr;
/* Simple loop variable: */
int loop;
...
/* Get the address of the IOTArray: */
ptr = (UBYTE *) &(ioreq->io_PTermArray);
/* Set all eight end of file characters: */
for( loop=0; loop < 8; loop++ )
{
/* Copy character after character: */
*ptr = eof_chars[ loop ];
/* Step one byte forward: */
ptr++;
}
Once you have set all desired parameters in the request block (IOExtPar
structure) you set the IO command to PDCMD_SETPARAMS and tell the parallel
device to do your request by calling the DoIO() function. If something went
wrong DoIO() will return with an error number, else 0 is returned which means
that your request have successfully been executed. (See below for a complete
list of error messages.) Here is an example:
/* We want to set the parallel device's parameters: */
ioreq->IOPar.io_Command = PDCMD_SETPARAMS;
/* Do our request, and return when done: */
error = DoIO( ioreq );
8.3.4 WRITE DATA
To send data to the parallel device you have to:
1. Set the command flag "CMD_WRITE" in the "io_Command" field.
2. Tell the parallel device how many bytes you want to send by setting the
"io_Length" field as desired. If you set it to -1 the device will continuously
send data and stop first when an end-of-file character is found. (The
end-of-file character will also be sent.) Note that the parallel flag
"PARF_EOFMODE" must have been set, or the device will never stop sending data.
3. Give the field "io_Data" the address of the first byte of data that should
be sent.
4. Finally you send the request to the parallel device. If you want to wait for
the request to be completed (synchronous) you should use the DoIO() function.
On the other hand, if you want that your program continues to work while your
request is completed, you should use the SendIO() function.
Here is an example on how to write data to the parallel port: (In this example
the program will be put to sleep while the data is sent to the parallel port.)
error:
(UBYTE) is a simple unsigned byte variable.
ioreq:
(struct IOExtPar *) is a pointer to an IOExtPar structure.
data:
(BYTE) is a pointer to a block of memory where all data you want to send is
located.
/* We want to send (write) some data: */
ioreq->IOPar.io_Command = CMD_WRITE;
/* Give the start address of our data buffer: */
ioreq->IOPar.io_Data = data;
/* We want to send 150 bytes/characters: */
ioreq->IOPar.io_Length = 150;
/* Do our request: */
error = DoIO( ioreq );
/* Everything OK? */
if( error )
printf( "Problems while writing!\n" );
If you do not want to wait for the request to be completed you should use the
SendIO() function instead. To check later if therequest have been completed or
not you can either use the function CheckIO(), or wait for a message to arrive
at the request block's reply port. (Since other request may also send messages
to this port, it is usually easiest to use CheckIO().)
Once the request have been completed you can look at the io_Error field of the
request block to check if everything was OK. If the field is zero everything
was executed without any problems, but if it is non zero the request failed.
Here is an example:
/* Declare a pointer and set it to NULL: */
struct IOExtPar *ptr = NULL;
...
/* We want to send (write) some data: */
ioreq->IOPar.io_Command = CMD_WRITE;
/* Give the start address of our data buffer: */
ioreq->IOPar.io_Data = data;
/* We want to send 280 bytes/characters: */
ioreq->IOPar.io_Length = 280;
/* Do our request and return immediately: */
SendIO( ioreq );
/* As long as the pointer is not pointing to */
/* the request we should stay in the loop: */
while( ptr == NULL )
{
... do something ...
/* Check if the request has been completed: (If the */
/* request has been completed CheckIO() will return */
/* a pointer to the request, else NULL is returned.) */
ptr = CheckIO( ioreq );
}
/* Remove the requst block's message. (The ptr and ioreq */
/* are in this example identical, so it does not matter */
/* whichever you will use. The parenthesis around the */
/* expression is actually unnecessary, but this looks */
/* better.) */
Remove( &(ptr->IOPar.io_Message.mn_Node) );
/* Everything OK? Check the io_Error filed: */
if( ioreq->IOPar.io_Error )
printf( "Problems while writing!\n" );
8.3.5 READ DATA
Although most people use the parallel port to send data to their printer, more
and more users connect other external devices like video digitzers and sound
samplers. With these devices you do not only need to send data, you also have
to collect data from the parallel port.
The process of collecting (reading) is very similar to reading. Here is what
you have to do:
1. Set the command flag "CMD_READ" in the "io_Command" field.
2. Tell the parallel device how many bytes you want to read by setting the
"io_Length" field as desired. If you set it to -1 the device will continuously
read data and stop first when an end-of-file character is received. (The
end-of-file character will also be collected.) Note that the parallel flag
"PARF_EOFMODE" must have been set, or the device will never stop collecting
data.
3. Give the field "io_Data" a pointer to your data buffer where all collected
data should be placed. Note that the buffer must be big enough so all data will
fit!
4. Finally you send the request to the parallel device. If you want to wait for
the request to be completed (synchronous) you should as usual use the DoIO()
function. On the other hand, if you want that your program continues to work
while your request is completed, you should use the SendIO() function.
Here is an example on how to read data: (Your program will be put to sleep
while the data is collected.)
error:
(UBYTE) is a simple unsigned byte variable.
ioreq:
(struct IOExtPar *) is a pointer to an IOExtPar structure.
data:
(BYTE) is a pointer to a block of memory where all data which is collected will
be stored. Note that the data buffer must be big enough so all data will fit.
/* We want to read some data: */
ioreq->IOPar.io_Command = CMD_READ;
/* Give the start address of our data buffer: */
ioreq->IOPar.io_Data = data;
/* We want to read 400 bytes/characters: (The buffer must */
/* then be at least 400 bytes.) */
ioreq->IOPar.io_Length = 400;
/* Do our request: */
error = DoIO( ioreq );
/* Everything OK? */
if( error )
printf( "Problems while reading!\n" );
The program above will go to sleep while the data is collected from the
parallel port. If you want to do something while the data is fetched and not go
to sleep you should use the asynchronous function SendIO() function as
explained above. Here is an example:
/* Declare a pointer and set it to NULL: */
struct IOExtPar *ptr = NULL;
...
/* We want to read some data: */
ioreq->IOPar.io_Command = CMD_READ;
/* Give the start address of our data buffer: */
ioreq->IOPar.io_Data = data;
/* We want to read 400 bytes/characters: (The buffer must */
/* then be at least 400 bytes.) */
ioreq->IOPar.io_Length = 400;
/* Do our request and return immediately: */
SendIO( ioreq );
/* As long as the pointer is not pointing to */
/* the request we should stay in the loop: */
while( ptr == NULL )
{
... do something ...
/* Check if the request has been completed: (If the */
/* request has been completed CheckIO() will return */
/* a pointer to the request, else NULL is returned.) */
ptr = CheckIO( ioreq );
}
/* Remove the requst block's message. (The ptr and ioreq */
/* are in this example identical, so it does not matter */
/* whichever you will use. The parenthesis around the */
/* expression is actually unnecessary, but this looks */
/* better.) */
Remove( &(ptr->IOPar.io_Message.mn_Node) );
/* Everything OK? Check the io_Error filed: */
if( ioreq->IOPar.io_Error )
printf( "Problems while reading!\n" );
8.3.6 HOW TO HANDLE SEVERAL REQUESTS SIMULTANIOUSLY
Since you usually will want to read and write data at the same time, you have
to use two separate request blocks. If you are using shared access mode you
simply create two request blocks and open the parallel device twice. (Do not
forget to close both requests later on.) Here is an example:
struct MsgPort *replymp;
struct IOExtPar *parallel_req_read;
struct IOExtPar *parallel_req_write;
UBYTE error;
/* We use only one reply message port: */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* Create the request block "read": */
parallel_req_read = (struct IOExtPar *)
CreateExtIO( replymp, sizeof( struct IOExtPar ) );
if( !parallel_req_read )
clean_up( "Not enough memory!" );
parallel_req_read->io_ParFlags = PARF_EOFMODE | PARF_SHARED;
/* Create the request block "write": */
parallel_req_write = (struct IOExtPar *)
CreateExtIO( replymp, sizeof( struct IOExtPar ) );
if( !parallel_req_write )
clean_up( "Not enough memory!" );
parallel_req_write->io_ParFlags = PARF_EOFMODE | PARF_SHARED;
/* Open the parallel device for the read request: */
error = OpenDevice( PARALLELNAME, 0, parallel_req_read, 0 );
if( error )
clean_up( "Could not open the parallel device (Read)!" );
/* Open the parallel device for the write request: */
error = OpenDevice( PARALLELNAME, 0, parallel_req_write, 0 );
if( error )
clean_up( "Could not open the parallel device (Write)!" );
If you are using exclusive access mode you can of course not open the parallel
device twice. Instead we have to copy the first request block to the other
request block, byte for byte. Here is an example:
struct MsgPort *replymp;
struct IOExtPar *parallel_req_read;
struct IOExtPar *parallel_req_write;
BYTE *r_ptr;
BYTE *w_ptr;
UBYTE error;
int loop;
/* We use only one reply message port: */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* Create the request block "read": */
parallel_req_read = (struct IOExtPar *)
CreateExtIO( replymp, sizeof( struct IOExtPar ) );
if( !parallel_req_read )
clean_up( "Not enough memory!" );
parallel_req_read->io_ParFlags = PARF_EOFMODE | PARF_SHARED;
/* Create the request block "write": */
parallel_req_write = (struct IOExtPar *)
CreateExtIO( replymp, sizeof( struct IOExtPar ) );
if( !parallel_req_write )
clean_up( "Not enough memory!" );
parallel_req_write->io_ParFlags = PARF_EOFMODE | PARF_SHARED;
/* Open the parallel device for the read request: */
error = OpenDevice( PARALLELNAME, 0, parallel_req_read, 0 );
if( error )
clean_up( "Could not open the Parallel Device (Read)!" );
/* Since we can not open the parallel device once again */
/* for the write request, we have to copy the whole */
/* read request block into the write request block. */
/* Get the start address of both request blocks: */
r_ptr = (BYTE *) parallel_req_read;
w_ptr = (BYTE *) parallel_req_write;
/* Copy the request block, byte by byte: */
for( loop=0; loop < sizeof( struct IOExtPar ); loop++ )
{
/* Copy one byte: */
*w_ptr = *r_ptr;
/* Step one byte forward: */
w_ptr++;
r_ptr++;
}
Now we have two request blocks, and can therefore use both read and write
requests at the same time. If you also want to use the special extra functions
described further down, you may want to create even one more request block (or
maybe even more...). The procedure is the same as described above.
8.3.7 ERRORS
While you are using the parallel device you may sometimes encounter an error
message. Usually it is easy to guess what went wrong, but it is always good to
check what really happened. There exit for the moment seven different types of
parallel port errors, all defined in the header file "devices/parallel.h".
You will either receive the error message from the function you just called
(for example, DoIO() returns 0 or an error number), or you can check the
request block to see if there were any problems. (The io_Error filed of the
request block either contains 0, which means everything is OK, or an error
number.)
Here is a complete list of the parallel error messages:
ParErr_DevBusy:
Some other task/request is already using the parallel device.
ParErr_BufTooBig:
Buffer too big. Hey, what is this?
ParErr_InvParam:
The request block's parameters were not properly initialized.
ParErr_LineErr:
There were some problems with the communication. The parallel cable is faulty
or the other device is not properly initialized or not connected. Tell the user
to check the cables!
ParErr_NotOpen:
The parallel device was not open!
ParErr_PortReset:
The parallel device have just been resetted. Someone just sent a CMD_Reset
request. Default parameters are set.
ParErr_InitErr:
The parallel device could not be initialized with your requirements. (Probably
forgot to clear all unused flags.)
While you are using the parallel device it may happen that you also receive
error messages from Exec. (Exec is handling all stuff like messages, requests,
tasks and so on.) Here is a complete list of exec error messages: (defined in
the header file "exec/errors.h")
IOERR_OPENFAIL
The device (unit) could not be opened. (If you are denied access to the
parallel device you should receive the ParErr_DevBusy flag instead of this exec
message, but internally this flag is used.)
IOERR_ABORTED
When you abort a previously started request by calling the AbortIO() function,
the io_Error filed of that request is set to IOERR_ABORTED. If you find a
request block with this flag set, you know that it has been aborted.
IOERR_NOCMD
You tried to use a command that is not supported by the parallel device.
IOERR_BADLENGTH
The length of the request was not valid.
8.3.8 CLEAN UP
As usual on the Amiga you must remember to close and return everything you have
opened or allocated. If you do not close the parallel device after you, other
programs will then not be able to use the it. PLEASE be very careful about
this!
The routine is very similar to how you close the serial device which was
described in the previous chapter. However this is so important that it can not
be said too often.
Here is a list of what you have to do:
1. All requests you have started with SendIO() or BeginIO() (asynchronous
commands) must either have been completed or aborted before you may close the
device. It is a very common error to forget this, and it can be hard to find
this bug. Usually the program will work fine (the command was completed in
time), but now and then your program will crash (the command was completed
after the device have been closed).
A simple way is to abort all commands that have not reported that they have
been completed, but this is not always good way to do it. (The last commands
may be important and should therefore not be aborted.)
If you do not want to abort the command, you should instead wait for it to be
completed. The WaitIO() function is simple to use, and will put your program to
sleep while waiting, so no computer time is wasted. If the request has already
been completed, the function will return immediately. WaitIO() will also remove
the message from the reply port. It is a very useful and simple function to
use, but do NOT try to wait for a request that has not been started!
Here is an example on how to wait for a request to be completed: (If the
request already has been completed it does not matter, WaitIO() will then
simply return immediately. Note that we do not have to remove any messages from
the reply port if we use WaitIO().)
/* Store possible error numbers here: */
UBYTE error;
/* ... */
/* Wait for the request to be completed: */
error = WaitIO( ioreq );
/* Everything OK? */
if( error )
printf( "Something went wrong!" );
/* Well, successful or not, we may now */
/* close the device! */
To abort a request, simply use the AbortIO() function:
/* Try to abort a previously started request: */
/* (Do not try to abort a request that has not */
/* been started.) */
AbortIO( ioreq );
2. When all requests have been completed or aborted you may close the parallel
device. (Requests that have been executed by calling the DoIO() function have
already been completed before your program wakes up, and thus you do not need
to wait for these.)
The parallel device is closed as all other devices, by calling the
CloseDevice() function. Here is an example:
/* Close the Parallel Device: */
if( !parallel_dever )
CloseDevice( ioreq );
3. You should now return all request blocks you have allocated. If you
allocated a standard sized request block by calling the CreateStdIO() function,
you should free it by calling the DeleteStdIO() function. However, if you have
allocated an extended request block (like the parallel device's request blocks)
by calling the CreateExtIO() function, you MUST call the DeleteExtIO(), and of
course remove the same amount of data as you allocated.
How to delete a standard request block: (Since it is of the standard size, you
do not need to specify any size.)
/* Deallocate a standard sized request block: */
DeleteStdIO( ioreq );
With extended request block you have to specify the size
as you did when you allocated it:
/* Deallocate an extended request block: */
/* (The size may vary depending on what */
/* device it was used for.) */
DeleteExtIO( ioreq, sizeof( struct IOExtSer ) );
Note that ALL request blocks that have been allocated, must be removed!
4. Finally you should close all message ports you have previously opened.
Simply use the DeletePort() function as this example demonstrates:
/* Remove the replyport: */
DeletePort( replymp);
Please be careful with how your program terminates! Your program should not
only run fine, but it should also allow other programs to run after and
simultaneously. Remember that your program must also be able to quit nice and
neatly even if it had to terminate too early because of some fatal error. The
cleaning up should only be done where it is needed, and if youhave not
allocated the memory or opened the device before your program quits, you should
of course NOT try to free these resources! If you do the Amiga will most
certainly crash! Too many programs contain this very annoying error. Make sure
yours will not be the same.
The best way to manage this cleaning up routine is to write a separate function
which checks each thing before it frees it. The idea is that you may call this
routine at any time, and it will still manage to clear everything properly.
Here is an example: (Note how we check each thing to see if it should be
removed!)
/* Close and return everything that has been */
/* opened and allocated before we quit: */
void clean_up()
{
/* 1. Close the Parallel Device: */
if( !parallel_dever )
CloseDevice( parallel_req );
/* 2. Deallocate the parallel request block: */
if( parallel_req )
DeleteExtIO( parallel_req, sizeof( struct IOExtPar ) );
/* 3. Remove the replyport: */
if( replymp )
DeletePort( replymp);
/* 4. Quit: */
exit( 0 );
}
Comments:
1. The parallel_dever is a variable that we have declared ourself, and is used
only to determine if the device has been opened or not. (The variable is set to
TRUE before the program starts, and is only changed to FALSE after a successful
opening of the device. See the included examples for more information.) We
should of course only close the device if the parallel_dever is FALSE.
("parallel_dever" stands for "parallel device error".)
2. The parallel_req is a pointer which is either pointing to a request block,
or NULL if no request block has been allocated. We should of course only free
the request block if there exist one.
3. The replymp is a pointer which is either pointing to a message port, or NULL
if no message port has been opened. And of course, we should only close the
message port if it has been opened.
4. Finally our program may terminate. The exit() function is placed here so we
are sure the program will quit. (If we did not have this exit(), our function
would then return the control to our program again, and then we would be in
deep s*#^%.)
8.4 A COMPLETE EXAMPLE
Here is a complete example that opens the parallel device, sets all necessary
parameters, sends some data, collects some data (well actually only 0 bytes)
and finally cleans up and quits. Since we send the read and write requests
after each other (the first is completed before the other is started) we only
need to use one request block.
#include <exec/types.h>
#include <devices/parallel.h>
/* Declare a pointer to our reply port: */
struct MsgPort *replymp = NULL;
/* Declare a pointer to our parallel request block: */
struct IOExtPar *parallel_req = NULL;
/* Store the parallel device error here: */
UWORD parallel_dever = TRUE;
/* Declare our own data buffer: */
/* (Last byte is a NULL sign.) */
BYTE buffer[] = "Anders Bjerin was here...";
/* Declare our functions: */
void main();
void clean_up( STRPTR text );
void main()
{
int loop;
UBYTE *ptr;
UBYTE error;
/* The eight end-of-file characters: */
UBYTE eof_char[8]={ 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00 };
/* OPEN THE PARALLEL DEVICE: */
/* Get a reply port: (No name, priority 0) */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* Create a parallel request block: */
parallel_req = (struct IOExtPar *)
CreateExtIO( replymp, sizeof( struct IOExtPar ) );
if( !parallel_req )
clean_up( "Not enough memory for the parallel request block!" );
/* Since we want exclusive access mode, we do not set the */
/* parallel flag "PARF_SHARED", which otherwise must have */
/* been set before the device is opened. To make sure the */
/* field is empty we set it to 0: */
parallel_req->io_ParFlags = 0;
/* Open the Parallel Device: */
parallel_dever = OpenDevice( PARALLELNAME, 0, parallel_req, 0 );
if( parallel_dever )
clean_up( "Could not open the Parallel Device!" );
/* SET THE REST OF THE PARALLEL PARAMETERS (not many): */
/* Check for end-of-file characters: */
parallel_req->io_ParFlags = PARF_EOFMODE;
/* No additional flags: */
parallel_req->io_PExtFlags = NULL;
/* Set all eight end of file characters: */
ptr = (UBYTE *) &(parallel_req->io_PTermArray);
for( loop=0; loop < 8; loop++ )
{
/* Copy character after character: */
*ptr = eof_char[ loop ];
/* Step one byte forward: */
ptr++;
}
/* All values have now been set, lets */
/* do a PDCMD_SETPARAMS request: */
parallel_req->IOPar.io_Command = PDCMD_SETPARAMS;
/* Do our request: */
error = DoIO( parallel_req );
if( error )
clean_up( "Could not set the parallel parameters!" );
/* SEND DATA TO THE PARALLEL PORT: */
/* We want to send (write) some data: */
parallel_req->IOPar.io_Command = CMD_WRITE;
/* Give the start address of our data: */
parallel_req->IOPar.io_Data = (APTR) buffer;
/* We want to send data until we find a NULL sign: */
/* (The NULL sign was specified in the EOF-char.) */
parallel_req->IOPar.io_Length = -1;
/* Do our request: */
error = DoIO( parallel_req );
if( error )
clean_up( "Could not send data to the parallel port!" );
/* READ DATA FROM THE PARALLEL DEVICE: */
/* We want to read some data: */
parallel_req->IOPar.io_Command = CMD_READ;
/* Give the start address of our buffer: */
parallel_req->IOPar.io_Data = (APTR) buffer;
/* We want to read 0 bytes: */
/* (Since most of you do not have anything which sends data */
/* to the parallel device, I do not want to wait for more */
/* than 0 bytes to arrive.) */
parallel_req->IOPar.io_Length = 0;
/* Do our request: */
error = DoIO( parallel_req );
if( error )
clean_up( "Could not read data from the parallel port!" );
/* THE END: */
clean_up( "The End" );
}
/* Close and return everything that has been */
/* opened and allocated before we quit: */
void clean_up( STRPTR text )
{
/* Close the Parallel Device: */
if( !parallel_dever )
CloseDevice( parallel_req );
/* Deallocate the parallel request block: */
if( parallel_req )
DeleteExtIO( parallel_req, sizeof( struct IOExtPar ) );
/* Remove the replyport: */
if( replymp )
DeletePort( replymp);
/* Print the message: */
printf( "\n%s\n", text );
/* Quit: */
exit( 0 );
}
8.5 OTHER USEFUL COMMANDS
Although reading and writing are most commonly used commands, there exist some
other functions that can sometimes be needed. Here is a complete list of
commands that can be sent with help of a request block:
1. Flush, removes all queued requests.
2. Query, get some information from the parallel device.
3. Reset, reinitializes the parallel device.
4. Start, restarts the parallel communication.
5. Stop, temporary stops the parallel communication.
8.5.1 FLUSH
If several requests are sent to the parallel device they are all queued on a
FIFO (First In First Out) basis. The command "CMD_FLUSH" can then be used to
remove all these queued commands. Here is an example:
/* We want to remove all queued requests: */
ioreq->IOPar.io_Command = CMD_FLUSH;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not remove the queued requests!\n" );
8.5.2 QUERY
The command "PDCMD_QUERY" can be used to get some information from the parallel
device. It is useful if you want to see if the paper is out, printer is busy or
if it is currently writing or reading. All this can be found in the "io_Status"
field of the request structure, as this table shows:
Name Bit Hex Active Description
------------------------------------------------------------
IOPTF_PARBUSY 0 0x01 Low Printer is selected
IOPTF_PAPEROUT 1 0x02 Low Paper out
IOPTF_PARSEL 2 0x04 Low Printer is busy
IOPTF_RWDIR 3 0x08 - Reading (0) or Writing (1)
- 4 0x10 - Reserved
- 5 0x20 - Reserved
- 6 0x40 - Reserved
- 7 0x80 - Reserved
Here is an example:
/* Check the parallel device: */
ioreq->IOPar.io_Command = PDCMD_QUERY;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not get any information from the device!\n" );
else
{
/* Check the "io_Status" field: */
if( ioreq->io_Status & IOPTF_PARBUSY )
printf( "Printer is busy.\n" );
if( ioreq->io_Status & IOPTF_PAPEROUT )
printf( "Paper out!\n" );
if( ioreq->io_Status & IOPTF_PARSEL )
printf( "Printer selected!\n" );
printf( "Device is %s\n",
ioreq->io_Status & IOPTF_RWDIR ? "Writing" : "Reading" );
}
8.5.3 RESET
Send the command "CMD_RESET" to reset the parallel device. All commands that
are queued to the device will be removed, commands that are currently executed
will be aborted all parallel flags are resetted. Here is an example:
/* We want to reset the parallel device: */
ioreq->IOPar.io_Command = CMD_RESET;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not reset the parallel device!\n" );
8.5.4 START
After you have stopped the parallel communication by sending an CMD_STOP
command, you may want to start the communication again. It is done by sending a
CMD_START command. Here is an example:
/* We want to start parallel communication again: */
ioreq->IOPar.io_Command = CMD_START;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not start the parallel communication!\n" );
8.5.5 STOP
To temporary stop all parallel communication you send a CMD_STOP command. The
communication will then first start again when a CMD_START command is
broadcasted. Here is an example:
/* We want to temporary stop all parallel communication: */
ioreq->IOPar.io_Command = CMD_STOP;
/* Do our request: */
error = DoIO( ioreq );
/* OK? */
if( error )
printf( "Could not stop the parallel communication!\n" );
8.6 FUNCTIONS
DoIO()
DoIO() is used to send requests to a device, and waits for it to be completed.
While the program is waiting it is put to sleep so it will not waste any
computer time. DoIO() will return first when the request have been completed or
failed, and no message is therefore sent to the reply port.
Synopsis:
error = DoIO( req );
error:
(long) DoIO() will return first when the request has been completed or
something has failed. If the request was successfully completed zero is
returned, else an error number is returned. What error number depends on which
device was used.
req:
(struct IORequest *) Pointer to the request you want to have executed.
SendIO()
SendIO() is used to send requests to a device, but will return immediately
without any delay. To check if the request have been completed use the
CheckIO() function, or look at the request's reply port for any messages. Once
the request has been completed you must remove the message at the reply port.
(CheckIO() will not do it.) To remove a message use the function Remove(). Note
that you may NOT close the device before all requests have been completed or
aborted!
Synopsis:
SendIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed.
CheckIO()
CheckIO() is used to check if a previously started request has been completed.
Note that this function will not remove the message at the reply port. This
must be done with the Remove() function.
Synopsis:
ptr = CheckIO( req );
ptr:
(long) CheckIO() will either return NULL if the request have not been completed
or it will return a pointer to the request block.
req:
(struct IORequest *) Pointer to the request you want to check.
WaitIO()
WaitIO() will wait for the request to be completed, and while the program is
waiting it is put to sleep so no computer time is wasted.
Synopsis:
error = WaitIO( req );
error:
(long) WaitIO() will return first when the request, that has previously been
sent, has been completed or something has failed. If the request was
successfully completed zero is returned, else an error number is returned. What
error number depends on which device was used.
req:
(struct IORequest *) Pointer to the request you want to wait for to be
completed. Note that the request must have already been sent to the device by
either a SendIO() or BeginIO() function call.
BeginIO()
BeginIO() is a low level form of the SendIO() function. The advantage with
BeginIO() is that no fields of the request block will be altered as which is
the case with SendIO(). With the parallel device you should only use SendIO().
Synopsis:
BeginIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed.
AbortIO()
AbortIO() will try to abort a previously started request. This function should
be used sparsely since it does not look so good if you start a request and the
try to stop it. (Better not start it at all.) However, it is easy, and can
sometimes be very useful.
A request that is aborted will have its io_Error field set to IOERR_ABORTED
(defined in header file "exec/errors.h").
Synopsis:
AbortIO( req )
req:
(struct IORequest *) Pointer to the request you want to abort.
CloseDevice()
CloseDevice() will (surprise!) close a device. Note that you should NOT close
the device before all started asynchronous requests have either been completed
or aborted.
Synopsis:
CloseDevice( ioreq );
ioreg:
(struct IORequest *) Pointer to the device's request block.
OpenDevice()
OpenDevice() will try to open the specified device.
Synopsis:
error = OpenDevice( name, unit, req, flags );
error:
(long) If OpenDevice() managed to open the device it returns 0, else an error
number is returned. If you try to open the parallel device, and there is
already a program that is using it, the error message "ParErr_DevBusy" is
returned.
name:
(char *) Name of the device you want to open. The name of the parallel device
is defined as PARALLELNAME in header file "devices/parallel.h".
unit:
(long) Which unit you want to open. Since there exist only one parallel port,
this field is ignored.
req:
(struct IORequest *) Pointer to a request block. For the parallel device it
must be a pointer to an extender parallel request block (struct IOExtPar).
flags:
(long) Any special mode is set here. Ignored by the parallel device.
8.7 COMMANDS
Here is a complete list of commands you may send to the parallel device. For
full documentation se examples above.
The special parallel device commands: (Defined in header file
"devices/parallel.h")
PDCMD_QUERY
Check the status of the parallel device.
PDCMD_SETPARAMS
Set the parameters of the parallel device.
The rest of the commands you may use are normal exec commands, and are defined
in header file "exec/io.h".
CMD_RESET
Resets all parameters of the parallel device.
CMD_READ
Read data from the parallel port.
CMD_WRITE
Write data to the parallel port.
CMD_STOP
Temporary stops all parallel communication.
CMD_START
Restarts parallel communication.
CMD_FLUSH
Removes all queued requests.
8.8 EXAMPLES
Example 1
This program demonstrates how you can use the Parallel Device. It does not do
very much since I do not know what you have connected to your parallel port,
but with small modifications you should be able to write your own parallel
communication packages.
Example 2
This example is rather similar to Example 1, but this time we do not wait for
the parallel port to complete our request. Instead we do somethings (well not
very much) and now and then checks if the request has been completed.
Example 3
This example is rather similar to Example 1, but this time we do not wait for
the parallel port to complete our request. We are also trying to read and write
at the same time. To be able to do several requests simultaneously we need one
request block for each command. In this example we use three separate request
blocks.
Example4
This example does not do anything, but it consists of several useful functions
that you can use yourself after small modifications. The functions demonstrates
all commands there exist for the parallel device, so if you had problems in
understanding how a command was used you can look here.
PRINTER DEVICE
9.1 INTRODUCTION
The printer device works close together with both the serial and the parallel
device. If you want to control a printer, there are several advantages of using
the printer device instead of the serial or parallel device.
First you do not need to know to which port (serial or parallel) the printer is
connected to. The printer device will simply look at the "Preferences"
settings, and will there find this type of information.
Secondly, since the user has specified what type of printer is used with
Preferences, the printer device will also know how to translate the universal
printer commands (listed below) into that printers own special commands. This
is very good since if your program can handle some of the commands listed
below, it will work on most printers.
Finally, there exist a lot of special features like printing graphics,
translating foreign characters, and changing the printer settings. All this is
supported by the printer device.
9.2 PRINTER DEVICE
The printer device is, as said above, very close connected to the serial and
parallel devices. The printer device can be said to be on a "higher level". The
reason of this is that the printer device will with help of Preferences control
either the serial or parallel device. (See illustration "Printer Device".)
----------------
|Printer Device|
----------------
|
V
-------------
|Preferences|
-------------
| |
V V
--------------- -----------------
|Serial Device| |Parallel Device|
--------------- -----------------
| |
V V
------------- ---------------
|Serial Port| |Parallel Port|
------------- ---------------
Although the complexity of printing, the printer device is very easy and
straight forward to use. It works like all other normal devices, and with only
some extra structures and commands a lot can be done.
9.2.1 THE PRINTER DEVICE'S REQUESTBLOCKS
As with all devices you use a request block to communicate with the printer
device. You initialize the request block as will be described below, and then
you simply send it to the device with for example an DoIO() or SendIO()
command.
Since the printer device can handle a lot of special commands there exist three
different request blocks. Which should be used depends on what you want to do.
1. The standard request block (struct IOStdReq) should be used for all "normal"
commands.
2. The special printer command block (struct IOPrtCmdReq) should be used when
you want to send special commands to the printer.
3. The special graphic request block (struct IODRPReq) should be used when you
want to print graphics (dump a Rastport to a printer).
To make it easier for you the printer device can handle a union with these
three structures. This means that you only have to bother about one singe
request block. The union look like this:
union printerIO
{
struct IOStdReq ios;
struct IODRPReq iodrp;
struct IOPrtCmdReq iopc;
};
Each structure of this union will be described in more detail below.
9.2.1.1 STANDARD REQUEST BLOCK
The printer device use a standard request block (struct IOStdReq) for all
normal commands. It looks like this: (Defined in header file "exec/io.h". Note
that you will probably only use some of these fields, so you do not need to
bother too much about it.)
struct IOStdReq
{
struct Message io_Message;
struct Device *io_Device;
struct Unit *io_Unit;
UWORD io_Command;
UBYTE io_Flags;
BYTE io_Error;
ULONG io_Actual;
ULONG io_Length;
APTR io_Data;
ULONG io_Offset;
};
io_Message:
This is the message that will be sent to your reply port once the device has
successfully or not finished your request
io_Device:
Pointer to the device which is using this request block.
io_Unit:
If the device has several units (like the Trackdisk Device) this is a pointer
to one of these units.
io_Command:
It is here you set the command flag which tells the device what you actually
want to do. There exist a lot of different commands depending on which device
you are using.
The commands can be divided into to groups. The first group of commands is the
standard device commands like read and write. They are used by most devices,
and are defined in the same header file as the request structure ("exec/io.h").
Se chapter 17 "Devices" for more information about these commands.
(The printer device can only handle the commands which are marked with a star
"*".)
CMD_RESET: (*) Reset the device. Removes all queued commands and sets all
fields to the default settings.
CMD_READ: Read (collect) data from the device.
CMD_WRITE: (*) Write (send) data to the device.
CMD_UPDATE: Update the device.
CMD_CLEAR: Clears the device's own input buffer.
CMD_STOP: (*) Temporarily stops the device.
CMD_START: (*) Restarts the device after it has been stopped.
CMD_FLUSH: (*) Removes all queued commands.
The second group contains all special device commands. Each device chapter
(18-29) contains a complete list of each device's special commands, together
with full instructions on how to use them.
The printer device uses four special commands, but only two of these may be
used by the standard request block: (See the other structures for more
information about the two other commands.)
PRD_RAWWRITE: Sends raw (not translated) characters.
PRD_QUERY: Send this command if you want to receive some information about
the printer device.
io_Flags:
This flag field is used by only some of the devices.
io_Error:
This field is set to 0 if the device managed to do your request, else an error
number is stored here. Depending on which device you are using different error
messages are used. However, there exist four standard error messages (defined
in header file "exec/errors.h"):
IOERR_OPENFAIL: Could not open the device.
IOERR_ABORTED: The request was aborted.
IOERR_NOCMD: A command that was not supported by the device was used.
IOERR_BADLENGTH: Bad length of the command - data.
If the printer device failed to do your request, one of these error flags is
returned:
PDERR_CANCEL: User cancelled the request.
PDERR_NOTGRAPHICS: The printer which the user has can not handle graphics.
PDERR_BADDIMENSION: The printer dimension is not valid.
PDERR_INTERNALMEMORY: Not enough memory for the printer device's internal
variables.
PDERR_BUFFERMEMORY: Not enough memory for the print buffer.
io_Actual:
The number of bytes that was actually collected or sent. If this number is not
the same as the number of bytes you wanted to collect/send, the communication
may have been interrupted.
io_Length:
The number of bytes you want to send or collect.
io_Data:
Pointer to where the data is which should be sent, or pointer to where all data
which is collected should be stored.
io_Offset:
Offset value used by some devices.
9.2.1.2 SPECIAL PRINTER COMMAND STRUCTURE
When you want to send special printer commands like "italics on", "boldface
off", "subscript on" etc (a complete table of printer commands is listed below)
you can do it in two ways:
(A) You send the printer command's "Escape Sequence" in the same way you send
normal text. These escape sequences will be translated by Preferences and then
sent to the printer.
(B) You send the printer command with help of a special printer command request
block (struct IOPrtCmdReq).
Both options will work equally well, but your code will look better if you use
the special IOPrtCmdReq structure. (It is easier to understand what is
happening, since escape sequences are usually not very informative and
definitely not easy to remember.) All printer commands are also defined in the
header file "devices/printer.h", and you are therefore recommended to use them
instead of the escape sequences.
struct IOPrtCmdReq
{
struct Message io_Message;
struct Device *io_Device;
struct Unit *io_Unit;
UWORD io_Command;
UBYTE io_Flags;
BYTE io_Error;
UWORD io_PrtCommand;
UBYTE io_Parm0;
UBYTE io_Parm1;
UBYTE io_Parm2;
UBYTE io_Parm3;
};
io_Message:
This is the message that will be sent to your reply port once the device has
successfully or not finished your request
io_Device:
Pointer to the printer device which is using this request block.
io_Unit:
Not used by the printer device.
io_Command:
Set this field to PRD_PRTCOMMAND, and the device will know that you want to
send commands to the printer. NOTE! The flag "PRD_PRTCOMMAND" should only be
used by IOPrtCmdReq structures!!!
io_Flags:
Not used.
io_Error:
This field is set to 0 if the printer device managed to do your request, else
an error number is stored here:
PDERR_CANCEL: User cancelled the request.
PDERR_NOTGRAPHICS: The printer which the user has can not handle graphics.
PDERR_BADDIMENSION: The printer dimension is not valid.
PDERR_INTERNALMEMORY: Not enough memory for the printer device's internal
variables.
PDERR_BUFFERMEMORY: Not enough memory for the print buffer.
io_PrtCommand:
The command you want to send to the printer. See below for a complete list of
all printer commands. Some of the commands require extra parameters, and these
parameters should be set in the following four fields.
io_Parm0:
First parameter.
io_Parm1:
Second parameter.
io_Parm2:
Third parameter.
io_Parm3:
Fourth parameter.
9.2.1.3 SPECIAL GRAPHIC STRUCTURE
When you want to print graphics you have to use the special IODRPReq structure.
It will dump a specified rastport (see chapter 12 "Low Level Graphics" for more
information about rastports) to the printer. Note that this request will only
work if the user has a printer that supports graphics.
The IODRPReq structure look like this: (also defined in header file
"devices/printer.h")
struct IODRPReq
{
struct Message io_Message;
struct Device *io_Device;
struct Unit *io_Unit;
UWORD io_Command;
UBYTE io_Flags;
BYTE io_Error;
struct RastPort *io_RastPort;
struct ColorMap *io_ColorMap;
ULONG io_Modes;
UWORD io_SrcX;
UWORD io_SrcY;
UWORD io_SrcWidth;
UWORD io_SrcHeight;
LONG io_DestCols;
LONG io_DestRows;
UWORD io_Special;
};
io_Message:
This is the message that will be sent to your reply port once the device has
successfully or not finished your request
io_Device:
Pointer to the printer device which is using this request block.
io_Unit:
Not used by the printer device.
io_Command:
Set this field to PRD_PRTCOMMAND, and the device will know that you want to
send commands to the printer. NOTE! The flag "PRD_PRTCOMMAND" should only be
used by IOPrtCmdReq structures!!!
io_Flags:
Not used.
io_Error:
This field is set to 0 if the printer device managed to do your request, else
an error number is stored here:
PDERR_CANCEL: User cancelled the request.
PDERR_NOTGRAPHICS: The printer which the user has can not handle graphics.
PDERR_BADDIMENSION: The printer dimension is not valid.
PDERR_INTERNALMEMORY: Not enough memory for the printer device's internal
variables.
PDERR_BUFFERMEMORY: Not enough memory for the print buffer.
io_RastPort:
Pointer to the RastPort that should be printed.
io_ColorMap:
Pointer to the ColorMap structure which contains all information about the
RastPort's colours.
io_Modes:
The ViewPort's display modes. The information is used to convert the picture
which will be printed to the correct aspects. (On a low resolution screen each
pixels is equally wide as tall. However, on a high resolution screen, each
pixel is only half as wide as it is tall. The same applies for interlaced and
non interlaced screens.) The printer device must also know if you want to print
a "normal" picture, or a picture with one of the special display modes like
"HAM" or "Extrahalf Brite". The following flags may be used:
HIRES: Set this flag if you want to print a high resolution screen. If this
flag is not set, the printer device assumes that you are using a low resolution
screen.
LACE: Set this flag if you want to print an interlaced picture. If this flag
is not set, the printer device assumes that you are using a non-interlaced
picture.
HAM: Set this flag if you want to print a "HAM" picture.
EXTRA_HALFBRITE: Set this flag if you want to print an "extra halfbrite"
picture.
D
UALPF: Set this flag if you want to print a dual playfields screen.
Note that the simplest way is to copy the Viewport structure's "modes" field.
You will then not risk to forget one or more display flags.
io_SrcX:
X offset of the source picture.
io_SrcY:
Y offset of the source picture.
io_SrcWidth:
Width of the source picture.
io_SrcHeight:
Height of the source picture.
io_DestCols:
Width of the printed picture. If the special flag "SPECIAL_MILCOLS" is set, the
width is in 1/1000". If the special flag "SPECIAL_FULLCOLS" is set, this field
is ignored by the printer device. (The picture will be as wide as possible.)
Finally, if the special flag "SPECIAL_FRACCOLS" is set this field specifies the
size as a fraction of the maximum width.
io_DestRows:
Height of the printed picture. If the special flag "SPECIAL_MILROWS" is set,
the height is in 1/1000". If the special flag "SPECIAL_FULLROWS" is set, this
field is ignored by the printer device. (The picture will be as tall as
possible.) Finally, if the special flag "SPECIAL_FRACROWS" is set this field
specifies the size as a fraction of the maximum height.
io_Special:
There exist several special graphical printing modes. Here is a complete list
of flags that may be used:
SPECIAL_MILCOLS: If this flag is set the "io_DestCols" field specifies the
width in 1/1000".
SPECIAL_MILROWS: If this flag is set the "io_DestRows" field specifies the
height in 1/1000".
SPECIAL_FULLCOLS: Set this flag if you want the width of the printed picture
to be as wide as possible.
SPECIAL_FULLROWS: Set this flag if you want the height of the printed picture
to be as tall as possible.
SPECIAL_FRACCOLS: If this flag is set the "io_DestCols" field specifies the
width as a fraction of the maximum width.
SPECIAL_FRACROWS: If this flag is set the "io_DestRows" field specifies the
height as a fraction of the maximum height.
SPECIAL_CENTER: Set this flag if you want the picture to be centered on the
paper.
SPECIAL_ASPECT: Set this flag if you want to use the correct aspect ratio
of the picture. If you set the flag "SPECIAL_FULLCOLS" and this
"SPECIAL_ASPECT" flag the printed picture will be as large as possible, but
still with the right aspects.
SPECIAL_DENSITY1: Set this flag if you want the picture to be printed with
the printer's lowest resolution.
SPECIAL_DENSITY2: Next resolution.
SPECIAL_DENSITY3: Next resolution.
SPECIAL_DENSITY4: Next resolution.
SPECIAL_DENSITY5: Next resolution.
SPECIAL_DENSITY6: Next resolution.
SPECIAL_DENSITY7: Use the printer's highest (finest) resolution.
SPECIAL_NOFORMFEED: Set this flag if you do not want that the paper is ejected
after each time you have printed graphics.
SPECIAL_TRUSTME: Set this flag if you do not want the printer to reset any
parameters while printing.
9.2.1.4 HOW TO CREATE THE PRINTER REQUEST BLOCK
To create a printer request block, use the CreateExtIO() function. Since there
exist three different types of structures that are needed, the printer device
assumes that you are using a union of these three structures. The union is
sadly not declared in any header file, so you have to declare it yourself. It
should look like this:
union printerIO
{
struct IOStdReq ios;
struct IODRPReq iodrp;
struct IOPrtCmdReq iopc;
};
The CreateExtIO function should always be used when you want to create a
request block which is not of the normal size (struct IOStdReq).
Synopsis
:
prt_req = CreateExtIO( msg_port, size );
prt_req:
(struct IORequest *) If CreateExtIO() managed to allocate and initialize the
request block it returns a pointer to it, else the function will return NULL.
(Note that you have to do some "casting" here, since the compiler expects that
the function returns a pointer to a normal IORequest structure, but this
function is used when you create other (larger) types of request block. Se
example for more information.)
msg_port:
(struct MsgPort *) Pointer to a message port to which the device will send a
message to each time it has completed your request.
size:
The size (in bytes) of the request block. Use the function sizeof() to find out
how big the special request block is. Example: sizeof( union printerIO );
9.2.2 OPEN THE PRINTER DEVICE
As with all devices you have to open a message port through which the printer
device can communicate with you, and allocate a request block (a printerIO
union), before you may open the device itself.
1. Open a message port: (Since it is only our task and the device that will use
the message port, we do not need to make it "public", hence no name. Priority
should as usual be set to 0, normal priority.)
struct MsgPort *replymp;
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
2. Allocate a request block of type printerIO union. Since the printer request
block is larger than normal request blocks (IOStdReq structure) it must be
created with the CreateExtIO() function.
union printerIO printer_req;
printer_req = (union printerIO *)
CreateExtIO( replymp, sizeof( union printerIO ) );
if( !printer_req )
clean_up( "Not enough memory!" );
3. Once the message port and the request block have successfully been created
you may open the printer device.
UBYTE error;
/* Open the printer device: */
error = OpenDevice( "printer.device", 0, printer_req, 0 );
if( error )
clean_up( "Could not open the Printer Device!" );
9.2.3 PRINT TEXT
When the printer device has been opened you may start to send text to the
printer. When printing normal text you should use the "IOStdReq" structure of
the printerIO union. You can either send text which will not be translated (raw
text) to the printer, or you can send text that will be translated.
The advantage of sending text that is translated by Preferences is that the
characters you send will also be the same when printed. (Many printers have
their own list of foreign characters. And if the characters are not translated,
you never know what will be printed.)
The second reason for translating text is that "escape sequences" (like
"underline on", "boldface off", "NLQ" etc...) will be translated into the
printer's own printer commands. If you write a program that uses "escape
sequences" you know that it will work on all printers (as long as they support
your commands). If you on the other hand send special untranslated printer
commands that are unlike for some type of printer models, your program will not
work with other types of printers.
Raw (untranslated) text should only be used when it is really needed (which is
not very often). If you want to dump the printers own ASCII codes it can be
useful to send untranslated characters, but otherwise I recommend you to only
send translated text.
To print text simply do like this:
1. Set the "io_Command" field to CMD_WRITE if you want the text to be
translated by Preferences. On the other hand, if you want to send raw
(untranslated) text use the "PRD_RAWWRITE" command.
2. Set the "io_Length" field to the number of characters (bytes) you want to
send.
3. Give the "io_Data" pointer the address of your data buffer which contains
the text you want to print.
4. Now send the request to the printer device, by either a DoIO() or SendIO()
function call.
If you want to wait for the device to finish your request before your program
continues you should use the "synchronous" command DoIO(). If you instead want
to continue to do something while the printer is working, you should use the
"asynchronous" command SendIO().
Here is an example on how to send text to the printer. This is a synchronous
request, which means the program is halted while the printer is working.
/* Pointer to an already initialized printer request block: */
union printerIO *printer_req
/* The text we want to print: (Oh no, not that text again!) */
BYTE data[13] = "Hello world!"
/* Store any error values here: */
BYTE error;
/* We want to print some translated text: (If we want */
/* to send raw, untranslated, text we should use the */
/* "PRD_RAWWRITE" command instead of "CMD_WRITE".) */
printer_req->ios.io_Command = CMD_WRITE;
/* Give the start address of our data: */
printer_req->ios.io_Data = (APTR) data;
/* Set number of chracters that should be printed: */
/* (12 characters - bytes) */
printer_req->ios.io_Length = 12;
/* Do our request: (This is a task sleep) */
error = DoIO( printer_req );
/* Check if the request was successfully executed: */
if( error )
printf( "Problems! Error code: %d\n", error );
Here is another example, but this time we use an asynchronous request, which
means the program continues to run while the printer is working.
/* Pointer to an already initialized printer request block: */
union printerIO *printer_req
/* The text we want to print: */
BYTE data[13] = "Hello again!"
/* Store any error values here: */
BYTE error;
/* Temporary pointer: */
union printerIO *ptr;
/* We want to print some translated text: (If we want */
/* to send raw, untranslated, text we should use the */
/* "PRD_RAWWRITE" command instead of "CMD_WRITE".) */
printer_req->ios.io_Command = CMD_WRITE;
/* Give the start address of our data: */
printer_req->ios.io_Data = (APTR) data;
/* Set number of chracters that should be printed: */
/* (12 characters - bytes) */
printer_req->ios.io_Length = 12;
/* Do our request and return immediately: */
SendIO( printer_req );
/* As long as the pointer is not pointing to */
/* the request we should stay in the loop: */
ptr = NULL;
while( ptr == NULL )
{
/* ... do something ... */
/* Well, I do not know what. */
/* Check if the request has been completed: (If the */
/* request has been completed CheckIO() will return */
/* a pointer to the request, else NULL is returned.) */
ptr = (union printerIO *) CheckIO( printer_req );
}
/* At last the request was completed! */
/* Remove the request block's message. (The ptr and */
/* printer_req are in this example identical, so it */
/* does not matter whichever you use. The parenthesis */
/* around the expression is actually unnecessary, but */
/* this looks better.) */
Remove( &(ptr->ios.io_Message.mn_Node) );
/* Check if everything is OK? */
if( ptr->ios.io_Error )
printf( "Problems while printing!\n" );
9.2.4 SEND SPECIAL COMMANDS TO THE PRINTER
Most printers can use different printing styles like underlined, italics and
boldface just to mention a few. Usually you can use several different fonts and
expand and compress the characters, as well as setting margins, use
proportional styles, handle foreign character sets etc...
All these features are controlled by special printer commands. Most printers
have their own set of unique commands. Luckily we do not need to know about
them. We simply use a set of universal printer commands listed below, and
Preferences will automatically translate these into the specified printer's own
commands.
There exist two ways to send the printer commands to the printer. Either you
send the "escape sequences" together with the rest of the text, or you can send
the commands with a separate request block (struct IOPrtCmdReq). Since escape
sequences are rather difficult to remember it usually looks better if you use
the separate request block technique.
When you send a printer command with help of the request block you should use
the IOPrtCmdReq structure. You set the io_Command field to "PRD_PRTCOMMAND",
the "io_PrtCommand" field to the command you want to send, and up to four
parameters may also be sent if needed by initializing the "io_Parm0" to
"io_Parm3" fields. Here is an example:
/* We want to send a printer command to the printer: */
printer_req->iopc.io_Command = PRD_PRTCOMMAND;
/* Set the printer command: [Underline On] */
printer_req->iopc.io_PrtCommand = aSGR4;
/* Set the parameters: */
printer_req->iopc.io_Parm0 = 0; /* Nothing */
printer_req->iopc.io_Parm1 = 0; /* Nothing */
printer_req->iopc.io_Parm2 = 0; /* Nothing */
printer_req->iopc.io_Parm3 = 0; /* Nothing */
/* Do the request: */
DoIO( printer_req );
Here is the complete list of printer commands and escape sequences. The names
are defined in header file "devices/printer.h".
If you use the escape sequences you should send the commands exactly as written
below with one difference - you should replace the character "n" with a (ASCII)
value. To set the top and bottom margins to 2 and 4, send the escape sequence:
"ESC[2;4r" (without quotations).
Name Command Esc Seq Function Defined by
--------------------------------------------------------------
aRIS 0 ESCc reset ISO
aRIN 1 ESC#1 initialize Amiga
aIND 2 ESCD lf ISO
aNEL 3 ESCE return,lf ISO
aRI 4 ESCM reverse lf ISO
aSGR0 5 ESC[0m normal char set ISO
aSGR3 6 ESC[3m italics on ISO
aSGR23 7 ESC[23m italics off ISO
aSGR4 8 ESC[4m underline on ISO
aSGR24 9 ESC[24m underline off ISO
aSGR1 10 ESC[1m boldface on ISO
aSGR22 11 ESC[22m boldface off ISO
aSFC 12 ESC[3nm set foreground color ISO
aSBC 13 ESC[4nm set background color ISO
aSHORP0 14 ESC[0w normal pitch DEC
aSHORP2 15 ESC[2w elite on DEC
aSHORP1 16 ESC[1w elite off DEC
aSHORP4 17 ESC[4w condensed fine on DEC
aSHORP3 18 ESC[3w condensed off DEC
aSHORP6 19 ESC[6w enlarged on DEC
aSHORP5 20 ESC[5w enlarged off DEC
aDEN6 21 ESC[6"z shadow print on DEC (sort of)
aDEN5 22 ESC[5"z shadow print off DEC
Name Command Esc Seq Function Defined by
--------------------------------------------------------------
aDEN4 23 ESC[4"z doublestrike on DEC
aDEN3 24 ESC[3"z doublestrike off DEC
aDEN2 25 ESC[2"z NLQ on DEC
aDEN1 26 ESC[1"z NLQ off DEC
aSUS2 27 ESC[2v superscript on Amiga
aSUS1 28 ESC[1v superscript off Amiga
aSUS4 29 ESC[4v subscript on Amiga
aSUS3 30 ESC[3v subscript off Amiga
aSUS0 31 ESC[0v normalize the line Amiga
aPLU 32 ESCL partial line up ISO
aPLD 33 ESCK partial line down ISO
Char set: or Typface:
aFNT0 34 ESC(B US 0 DEC
aFNT1 35 ESC(R French 1 DEC
aFNT2 36 ESC(K German 2 DEC
aFNT3 37 ESC(A UK 3 DEC
aFNT4 38 ESC(E Danish 4 DEC
aFNT5 39 ESC(H Sweden 5 DEC
aFNT6 40 ESC(Y Italian 6 DEC
aFNT7 41 ESC(Z Spanish 7 DEC
aFNT8 42 ESC(J Japanese 8 Amiga
aFNT9 43 ESC(6 Norweign 9 DEC
aFNT10 44 ESC(C Danish II 10 Amiga
aPROP2 45 ESC[2p proportional on Amiga
aPROP1 46 ESC[1p proportional off Amiga
aPROP0 47 ESC[0p proportional clear Amiga
aTSS 48 ESC[n E set proportional offset ISO
aJFY5 49 ESC[5 F auto left justify ISO
aJFY7 50 ESC[7 F auto right justify ISO
aJFY6 51 ESC[6 F auto full justify ISO
aJFY0 52 ESC[0 F auto justify off ISO
aJFY3 53 ESC[3 F letter space (justify) ISO (special)
aJFY1 54 ESC[1 F word fill(auto center) ISO (special)
aVERP0 55 ESC[0z 1/8" line spacing Amiga
aVERP1 56 ESC[1z 1/6" line spacing Amiga
aSLPP 57 ESC[nt set form length n DEC
aPERF 58 ESC[nq perf skip n (n>0) Amiga
aPERF0 59 ESC[0q perf skip off Amiga
aLMS 60 ESC#9 Left margin set Amiga
aRMS 61 ESC#0 Right margin set Amiga
aTMS 62 ESC#8 Top margin set Amiga
aBMS 63 ESC#2 Bottom marg set Amiga
aSTBM 64 ESC[n;nr T&B margins DEC
aSLRM 65 ESC[n;ns L&R margin DEC
aCAM 66 ESC#3 Clear margins Amiga
Name Command Esc Seq Function Defined by
--------------------------------------------------------------
aHTS 67 ESCH Set horiz tab ISO
aVTS 68 ESCJ Set vertical tabs ISO
aTBC0 69 ESC[0g Clr horiz tab ISO
aTBC3 70 ESC[3g Clear all h tab ISO
aTBC1 71 ESC[1g Clr vertical tabs ISO
aTBC4 72 ESC[4g Clr all v tabs ISO
aTBCAL L73 ESC#4 Clr all h & v tabs Amiga
aTBSAL L74 ESC#5 Set default tabs Amiga
aEXTEN D75 ESC[n"x extended commands Amiga
aRAW 76 ESC[n"r Next 'Pn' chars are raw Amiga
Suggested typefaces are:
0 - default typeface.
1 - Line Printer or equiv.
2 - Pica or equiv.
3 - Elite or equiv.
4 - Helvetica or equiv.
5 - Times Roman or equiv.
6 - Gothic or equiv.
7 - Script or equiv.
8 - Prestige or equiv.
9 - Caslon or equiv.
10 - Orator or equiv.
Amiga: Unique command for the Amiga.
ISO: Defined by the International Standards Organization.
DEC: Defined by Digital Equipment Corporation.
9.2.5 PRINT GRAPHICS
The printer device does not only handle text, it can also be used to print
graphics. To do this we have to use the IODRPReq structure of the request
block. We give it a pointer to a RastPort which should be printed and sets some
special values like size, resolution etc. The rest is then automatically done.
Note that some printers does not support graphics, and thus your request will
be returned immediately with an error value "PDERR_NOTGRAPHICS".
Here is what we have to do:
1. Set the command "PRD_DUMPRPORT".
2. Give the request block a pointer to the RastPort we want to print.
3. We must also give the request block a pointer to a ColorMap structure which
contains the picture's colour values.
4. Tell the request block what screen "mode" the RastPort is made for. The
information is used to convert the picture which will be printed to the correct
aspects.
On a low resolution screen each pixels is equally wide as tall. However, on a
high resolution screen, each pixel is only half as wide as it is tall. The same
applies for interlaced and non interlaced screens.
The printer device must also know if you want to print a "normal" picture, or a
picture with one of the Amiga's special display modes like "HAM" or "Extrahalf
Brite".
If you print a RastPort that is connected to a ViewPort, you can simply copy
the Viewport structure's "Modes" field. If you have defined a RastPort that is
not used to be displayed just to be printed, set the field to 0.
5. Tell the device if the whole RastPort should be printed, or just a part of
it.
6. You must also specify how large the printed picture should be. Note that
most printers have a much higher resolution than normal screens. You can
therefore usually print a picture that is much smaller than the screen, and
still have all details as the original.
7. Finally you can set some special graphical flags as described above.
Here is an example:
/* Store possible error numbers here: */
BYTE error;
/* An already initialized printer request block: */
union printerIO *printer_req,
/* Pointer to a RastPort: */
struct RastPort *rp,
/* Pointer to a ColourMap: */
struct ColorMap *cm,
/* We want to dump a RastPort to the printer: */
printer_req->iodrp.io_Command = PRD_DUMPRPORT;
/* Set a pointer to the RastPort structure: */
printer_req->iodrp.io_RastPort = rp;
/* Set a pointer to the ColorMap structure: */
printer_req->iodrp.io_ColorMap = cm;
/* Set the "display" modes: */
/* (0 = aspect 1:1, normal screen.) */
printer_req->iodrp.io_Modes = 0;
/* X position of the source: (10 pixels out) */
printer_req->iodrp.io_SrcX = 10;
/* Y position of the source: (10 pixels down) */
printer_req->iodrp.io_SrcY = 10;
/* Width of the source: (100 pixels wide) */
printer_req->iodrp.io_SrcWidth = 100;
/* Height of the source: (50 pixels high) */
printer_req->iodrp.io_SrcHeight = 50;
/* The width of the printed picture: (This field is in */
/* this example ignored by the printer device since the */
/* special flag "SPECIAL_FULLCOLS" is set. Maximum width.) */
printer_req->iodrp.io_DestCols = 0;
/* The height of the printed picture: (This field is in */
/* this example also ignored by the printer device since */
/* the special flag "SPECIAL_ASPECT" is set. The height */
/* will therefore be automatically calculated.) */
printer_req->iodrp.io_DestRows = 0;
/* Set the special printing commands: (Full width and */
/* the height is automatically set so the printed picture */
/* will have the right aspects.) */
printer_req->iodrp.io_Special =
SPECIAL_FULLCOLS | SPECIAL_ASPECT;
/* Do our request: */
error = DoIO( printer_req );
/* Check if the request was successfully executed: */
if( error )
printf( "Problems while printing!\n" );
9.2.6 ERRORS
When you are using the printer device it sometimes happens that your request
was not successfully executed, and you have encounter an error message. Usually
it is easy to guess what went wrong, but it is always good to check what really
happened.
You will either receive the error message from the function you just called
(for example, DoIO() returns 0 or an error number), or you can check the
request block to see if there were any problems. (The io_Error filed of the
request block either contains 0, which means everything is OK, or an error
number.)
Here is a complete list of the printer device error messages:
PDERR_CANCEL:
The user cancelled the request. If you receive this error you should not start
new printer requests until the user says he/she wants to print again. It is
very annoying if you have cancelled a printout but the program continues to
write new data to the printer. If you have issued several asynchronous requests
after each other and you receive this message, it is usually best to try to
remove the queued requests.
PDERR_NOTGRAPHICS:
The printer which the user has can not handle graphics.
PDERR_BADDIMENSION:
The dimension of the printout is not valid.
PDERR_INTERNALMEMORY:
Not enough memory for the printer device's internal variables.
PDERR_BUFFERMEMORY:
Not enough memory for the print buffer.
While you are using the printer device it may happen that you also receive
error messages from Exec. (Exec is handling all stuff like messages, requests,
tasks and so on.) Here is a complete list of exec error messages: (defined in
the header file "exec/errors.h")
IOERR_OPENFAIL:
The device (unit) could not be opened.
IOERR_ABORTED:
When you abort a previously started request by calling the AbortIO() function,
the "io_Error" filed of that request is set to "IOERR_ABORTED". If you find a
request block with this flag set, you know that it has been aborted.
IOERR_NOCMD:
You tried to use a command that is not supported by the printer device.
IOERR_BADLENGTH:
The length of the request was not valid.
9.2.7 CLEAN UP
As usual on the Amiga you must remember to close and return everything you have
opened or allocated. If you do not close the printer device after you a lot of
memory is wasted. The routine is very similar to how you close the parallel
device which was described in the previous chapter.
Here is a list of what you have to do:
1. All requests you have started with SendIO() or BeginIO() (asynchronous
commands) must either have been completed or aborted before you may close the
device. It is a very common error to forget this, and it can be hard to find
this bug. Usually the program will work fine (the command was completed in
time), but now and then your program will crash (the command was completed
after the device have been closed).
A simple way is to abort all commands that have not reported that they have
been completed, but this is not always good way to do it. (The last commands
may be important and should therefore not be aborted.)
If you do not want to abort the command, you should instead wait for it to be
completed. The WaitIO() function is simple to use, and will put your program to
sleep while waiting, so no computer time is wasted. If the request has already
been completed, the function will return immediately. WaitIO() will also remove
the message from the reply port. It is a very useful and simple function to
use, but do NOT try to wait for a request that has not been started!
Here is an example on how to wait for a request to be completed: (If the
request already has been completed it does not matter, WaitIO() will then
simply return immediately. Note that we do not have to remove any messages from
the reply port if we use WaitIO().)
/* Store possible error numbers here: */
UBYTE error;
/* ... */
/* Wait for the request to be completed: */
error = WaitIO( printer_req );
/* Everything OK? */
if( error )
printf( "Something went wrong!" );
/* Well, successful or not, we may now */
/* close the device! */
To abort a request, simply use the AbortIO() function:
/* Try to abort a previously started request: */
/* (Do not try to abort a request that has not */
/* been started!) */
AbortIO( printer_req );
2. When all requests have been completed or aborted you may close the printer
device. (Requests that have been started by calling the DoIO() function have
already been completed before your program wakes up, and thus you do not need
to wait for these.)
The printer device is closed as all other devices, by calling the CloseDevice()
function. Here is an example:
/* Close the Printer Device: */
CloseDevice( printer_req );
3. You should now return all request blocks you have allocated. The printer
request block (union printerIO) is an extended request block, and should be
deleted by the DeleteExtIO() function.
/* Deallocate the printer request block: */
DeleteExtIO( printer_req, sizeof(union printerIO) );
Note that ALL request blocks that have been allocated, must be removed!
4. Finally you should close all message ports you have previously opened.
Simply use the DeletePort() function as this example demonstrates:
/* Remove the replyport: */
DeletePort( replymp);
Please be careful with how your program terminates! Your program should not
only run fine, but it should also allow other programs to run after and
simultaneously. Remember that your program must also be able to quit nice and
neatly even if it had to terminate too early because of some fatal error. The
cleaning up should only be done where it is needed, and if you have not
allocated the memory or opened the device before your program quits, you should
of course NOT try to free these resources! If you do the Amiga will most
certainly crash! Too many programs contain this very annoying error. Make sure
yours will not be one of those.
9.3 A COMPLETE EXAMPLE
Here is a simple but complete example on how to print some text. For more
information see the Examples which are stored together with this document.
#include <exec/types.h> /* Data types. */
#include <exec/errors.h> /* Exec error messages. */
#include <devices/printer.h> /* Printer Device. */
#include <exec/io.h> /* Standard request block. */
/* Declare how the printer request block look like: */
union printerIO
{
struct IOStdReq ios;
struct IODRPReq iodrp;
struct IOPrtCmdReq iopc;
};
/* Declare a pointer to our reply port: */
struct MsgPort *replymp = NULL;
/* Declare a pointer our printer request block: */
union printerIO *printer_req = NULL;
/* Store the printer device error here: */
UWORD printer_dever = TRUE;
/* Declare our data buffer: (25 characters) */
BYTE buffer[] = "Anders Bjerin was here...";
/* Declare our functions: */
void main();
void clean_up( STRPTR text );
void main()
{
/* Error number: */
BYTE error;
/* Get a reply port: (No name, priority 0) */
replymp = (struct MsgPort *)
CreatePort( NULL, 0 );
if( !replymp )
clean_up( "Could not create the reply port!" );
/* Create the printer request block: */
printer_req = (union printerIO *)
CreateExtIO( replymp, sizeof(union printerIO) );
if( !printer_req )
clean_up( "Not enough memory for the printer request block!" );
/* Open the Printer Device: */
printer_dever = OpenDevice( "printer.device", 0, printer_req, 0 );
if( printer_dever )
clean_up( "Could not open the Printer Device!" );
/* We want to print some text: (translated) */
printer_req->ios.io_Command = CMD_WRITE;
/* Give the start address of our data: */
printer_req->ios.io_Data = (APTR) buffer;
/* Set number of chracters that should be printed: */
printer_req->ios.io_Length = 25;
/* Do our request: */
error = DoIO( printer_req );
/* Check if the request was successfully executed: */
if( error )
printf( "Problems while printing!" );
/* Clean up and quit: */
clean_up( "The End!" );
}
/* Close and return everything that has been */
/* opened and allocated before we quit: */
void clean_up( STRPTR text )
{
/* Close the Printer Device: */
if( !printer_dever )
CloseDevice( printer_req );
/* Deallocate the printer request block: */
if( printer_req )
DeleteExtIO( printer_req, sizeof(union printerIO) );
/* Remove the replyport: */
if( replymp )
DeletePort( replymp);
/* Print the message: */
printf( "\n%s\n", text );
/* Quit: */
exit( 0 );
}
9.4 OTHER USEFUL COMMANDS
There exist four more printer request commands that may sometimes be useful:
1. Flush, removes all queued requests.
2. Reset, reinitializes the printer device.
3. Start, restarts the printer communication.
4. Stop, temporary stops the printer communication.
9.4.1 FLUSH
If several requests are sent to the printer device they are all queued on a
FIFO (First In First Out) basis. The command "CMD_FLUSH" can then be used to
remove all these queued commands. Here is an example:
/* We want to remove all queued requests: */
printer_req->ios.io_Command = CMD_FLUSH;
/* Do our request: */
error = DoIO( printer_req );
/* OK? */
if( error )
printf( "Could not remove the queued requests!\n" );
9.4.2 RESET
Send the command "CMD_RESET" to reset the printer device. All commands that are
queued to the device will be removed, the command that is currently executed
will be aborted, and all printer flags are resetted. Here is an example:
/* We want to reset the printer device: */
printer_req->ios.io_Command = CMD_RESET;
/* Do our request: */
error = DoIO( printer_req );
/* OK? */
if( error )
printf( "Could not reset the printer device!\n" );
9.4.3 START
After you have stopped the printer communication by sending an CMD_STOP
command, you may want to start the communication again. It is done by sending a
CMD_START command. Here is an example:
/* We want to start printer communication again: */
printer_req->ios.io_Command = CMD_START;
/* Do our request: */
error = DoIO( printer_req );
/* OK? */
if( error )
printf( "Could not restart the printer communication!\n" );
9.4.4 STOP
To temporary stop all printer communication you send a CMD_STOP command. The
communication will then first start again when a CMD_START command is
broadcasted. Here is an example:
/* We want to temporary stop all printer communication: */
printer_req->ios.io_Command = CMD_STOP;
/* Do our request: */
error = DoIO( printer_req );
/* OK? */
if( error )
printf( "Could not stop the printer communication!\n" );
9.5 FUNCTIONS
DoIO()
DoIO() is used to send requests to a device, and waits for it to be completed.
While the program is waiting it is put to sleep so it will not waste any
computer time. DoIO() will return first when the request have been completed or
failed, and no message is therefore sent to the reply port.
Synopsis:
error = DoIO( req );
error:
(long) DoIO() will return first when the request has been completed or
something has failed. If the request was successfully completed zero is
returned, else an error number is returned. What error number depends on which
device was used.
req:
(struct IORequest *) Pointer to the request you want to have executed.
SendIO()
SendIO() is used to send requests to a device, but will return immediately
without any delay. To check if the request have been completed use the
CheckIO() function, or look at the request's reply port for any messages. Once
the request has been completed you must remove the message at the reply port.
(CheckIO() will not do it.) To remove a message use the function Remove(). Note
that you may NOT close the device before all requests have been completed or
aborted!
Synopsis:
SendIO( req )
req:
(struct IORequest *) Pointer to the request you want to have executed.
CheckIO()
CheckIO() is used to check if a previously started request has been completed.
Note that this function will not remove the message at the reply port. This
must be done with the Remove() function.
Synopsis:
ptr = CheckIO( req );
ptr:
(long) CheckIO() will either return NULL if the request have not been completed
or it will return a pointer to the request block.
req:
(struct IORequest *) Pointer to the request you want to check.
WaitIO()
WaitIO() will wait for the request to be completed, and while the program is
waiting it is put to sleep so no computer time is wasted.
Synopsis:
error = WaitIO( req );
error:
(long) WaitIO() will return first when the request, that has previously been
sent, has been completed or something has failed. If the request was
successfully completed zero is returned, else an error number is returned. What
error number depends on which device was used.
req:
(struct IORequest *) Pointer to the request you want to wait for to be
completed. Note that the request must have already been sent to the device by
either a SendIO() or BeginIO() function call.
AbortIO()
AbortIO() will try to abort a previously started request. This function should
be used sparsely since it does not look so good if you start a request and the
try to stop it. (Better not start it at all.) However, it is easy, and can
sometimes be very useful.
A request that is aborted will have its io_Error field set to IOERR_ABORTED
(defined in header file "exec/errors.h").
Synopsis:
AbortIO( req )
req:
(struct IORequest *) Pointer to the request you want to abort.
CloseDevice()
CloseDevice() will close a device. Note that you should NOT close the device
before all started asynchronous requests have either been completed or aborted.
Synopsis:
CloseDevice( ioreq );
ioreg:
(struct IORequest *) Pointer to the device's request block.
OpenDevice()
OpenDevice() will try to open the specified device.
Synopsis:
error = OpenDevice( name, unit, req, flags );
error:
(long) If OpenDevice() managed to open the device it returns 0, else an error
number is returned.
name:
(char *) Name of the device you want to open. The name of the printer device is
"printer.device".
unit:
(long) Set this field to 0.
req:
(struct IORequest *) Pointer to an already initialized printerIO union.
flags:
(long) Set this field to 0.
9.6 COMMANDS
Here is a complete list of commands you may send to the printer device. For
full documentation se examples above.
The special printer device commands: (Defined in header file
"devices/printer.h")
PRD_RAWWRITE
Send untranslated characters to the printer.
PRD_PRTCOMMAND
Send a printer command.
PRD_DUMPRPORT
Print graphics. (Dump a RastPort to the printer.)
The rest of the commands you may use are normal exec commands, and are defined
in header file "exec/io.h".
CMD_RESET
Resets all parameters of the printer device.
CMD_WRITE
Send data to the printer (through preferences).
CMD_STOP
Temporary stops all printer communication.
CMD_START
Restarts printer communication.
CMD_FLUSH
Removes all queued requests.
9.7 EXAMPLES
Example 1
This program demonstrates how you can use the Printer Device to send (raw as
well as translated) text to a printer.
Example 2
This program demonstrates how you can use the Printer Device to send (raw as
well as translated) text to a printer. However, instead of waiting for our
request to be completed as in Example 1, we use asynchronous requests.
Example 3
This program demonstrates how you can send printer commands to the Printer
Device, which will translate these commands with help of Preferences, before
they are sent to the printer.
Example 4
This example demonstrates how you can print graphics. It will dump the
workbench's Rastport to the printer.